https://d226lax1qjow5r.cloudfront.net/blog/blogposts/add-video-capabilities-to-zendesk-with-vonage-video-api/Blog_Zendesk_VideoAPI_1200x600.png

Hinzufügen von Video-Funktionen zu Zendesk mit der Video API von Vonage

Zuletzt aktualisiert am May 11, 2021

Lesedauer: 12 Minuten

In diesem Tutorial fügen wir Zendesk mithilfe der Video API von Vonage die Funktionen für Video, Bildschirmfreigabe und Aufzeichnung hinzu, damit Sie Ihren Kunden ein besseres Erlebnis bieten können.

Sie denken vielleicht, dass dies nichts für Sie ist, da Sie Zendesk nicht verwenden, aber tatsächlich gibt es viele andere Ticketsysteme, auf die Sie diese Erkenntnisse anwenden können. Wenn Sie das noch nicht überzeugt hat, zeigen wir Ihnen, wie Sie Aufzeichnungen programmgesteuert verarbeiten und in ein Zendesk-Ticket hochladen können, damit beide Parteien es herunterladen können.

Das Szenario

  • Die Kundin möchte ein offenes Ticket mit dem Support-Techniker besprechen. Sie fordert ein Video-Gespräch mit dem Support-Techniker an, indem sie auf die Schaltfläche Discuss Live with Javier und wartet darauf, dass er sich einschaltet.

A customer is requesting a call with the support agen

- Das Ticket wird mit einem internen Kommentar aktualisiert, so dass der Support-Techniker darüber informiert wird, dass der Antragsteller des Tickets eine Video-Sitzung wünscht.

Agent receives a notification that the customer requests a call

  • Der Support-Techniker nimmt an der Sitzung teil, sie gehen das Ticket durch (in diesem speziellen Fall gibt es nicht viel zu besprechen 😂). Sie beschließen, das Gespräch aufzuzeichnen, und sobald die Aufzeichnung gestoppt ist, wird sie in Form eines Ticketkommentars hochgeladen, damit beide Teilnehmer sie herunterladen können.

A recording of a video call between client and support engineer

Wenn dies Ihre Aufmerksamkeit erregt hat, folgen Sie uns bitte.

Architektur

Um einen Überblick über die Architektur dieser Integration zu geben, möchten wir Ihnen das folgende Diagramm zeigen:

Auf der einen Seite fordert der Endkunde über die Zendesk-Anforderungsseite ein Video-Gespräch mit dem Support-Techniker an. Der Server bearbeitet die Anfrage und aktualisiert das Ticket, um die Aufmerksamkeit des Agenten zu erhalten. Auf der anderen Seite nimmt der Agent, der Zendesk verwendet, an der gleichen Sitzung teil, um live zu diskutieren.

Voraussetzungen

Bevor wir loslegen, benötigen Sie Folgendes:

  1. Node.js installiert und einige Grundkenntnisse in JavaScript

  2. Ein Zendesk-Konto mit Administratorrechten

  3. Die Zendesk App Tools (ZAT) installiert

  4. Ein Amazon S3 Account

Vonage API-Konto

Um dieses Tutorial durchzuführen, benötigen Sie ein Vonage API-Konto. Wenn Sie noch keines haben, können Sie sich noch heute anmelden und mit einem kostenlosen Guthaben beginnen. Sobald Sie ein Konto haben, finden Sie Ihren API-Schlüssel und Ihr API-Geheimnis oben auf dem Vonage-API-Dashboard.

Zendesk Agent

Um mit Zendesk-Applikationen zu beginnen, können Sie die Erstellen Sie Ihre erste Support-App Tutorial folgen. Wechseln Sie in Ihr Projektverzeichnis und führen Sie den folgenden Befehl aus.

zat new

Sie werden aufgefordert, einige Informationen einzugeben, z. B. den Namen Ihrer Anwendung; wir nennen sie Zendesk Video-Anwendung. Außerdem werden Sie nach Ihrer E-Mail-Adresse und einigen anderen Parametern gefragt, die keinen Einfluss auf die Funktionalität haben. Sobald der Befehl ausgeführt wird, sehen Sie, dass die Anwendung erstellt wurde. Wir werden auch einen Ordner für unseren Server erstellen. Die endgültige Projektstruktur sieht wie folgt aus.

|--Application
    |-- Server
        |-- server.js
    |-- Zendesk Video App
        |-- manifest.json
        |-- Assets
          |-- iframe.html
          |-- index.css
          |-- index.jss

Unsere Anwendung besteht aus einem Rahmen, der in die Zendesk-Oberfläche eingebettet ist und einen Video-Chat-Bereich mit verschiedenen Aktionen enthält. Bearbeiten wir die iframe.html Datei, indem wir einige einfache Schaltflächenelemente hinzufügen, die es dem Agenten ermöglichen, einen Videoanruf mit dem Kunden innerhalb des Tickets zu führen. Sie können den folgenden Code in Ihre iframe.html:

 <!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/combine/npm/@zendeskgarden/css-bedrock@7.0.21,npm/@zendeskgarden/css-utilities@4.3.0">
  <link href="main.css" rel="stylesheet">
</head>
<body>
  

  <div id="content"></div>
  <button id="initiatesession" class="button" onclick="initializeSession()">Initiate Session</button>
  <button id="startPublishingVideoId"  class="button" onclick="startPublishingVideo()">Turn on Video </button>
  <button id="startPublishingScreenId" class="button" onclick="startPublishingScreen()">Share Screen</button>
  <button id="handleRecording" class="button" onclick="handleRecording()">Start Recording</button>
  
  <div id="videos" >
      
    <div id="publisher" ></div>
    <div id="subscriber" ></div>
 
  </div>
    
  <script id="requester-template" type="text/x-handlebars-template">

  </script>

  <script src="https://cdn.jsdelivr.net/npm/handlebars@4.3.3/dist/handlebars.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.min.js"></script>
  <script src="https://static.zdassets.com/zendesk_app_framework_sdk/2.0/zaf_sdk.min.js"></script>
  <script src="https://static.opentok.com/v2/js/opentok.min.js"></script>
  <script src="index.js"></script>
</body>
</html>

Wir werden auch einige grundlegende CSS für die Schaltflächen hinzufügen.

.button {
  background-color: #008CBA;;
  border: none;
  color: black;
  padding: 15px 32px;
  text-align: center;
  text-decoration: none;
  display: inline-block;
  font-size: 16px;
  margin: 4px 2px;
  cursor: pointer;
  border-radius: 12px;
}

Bearbeiten Sie nun die main.js Datei, die einen ZAF-Client instanziiert. Mit dem ZAF-Client kann Ihre Anwendung mit dem Zendesk-Hostprodukt kommunizieren. Sie können den Client in Ihren Anwendungen verwenden, um auf Ereignisse zu warten, Eigenschaften abzurufen oder festzulegen oder Aktionen aufzurufen. In diesem Fall sind wir an den Details des Tickets interessiert, an dem wir gerade arbeiten. Insbesondere die Ticket-ID und die ID des Anforderers. Sobald das Versprechen erfüllt ist, können wir eine Anfrage an unseren Server senden, um den API-Schlüssel, die Sitzungs-ID und ein Token für dieses Ticket zu erhalten. Die gesamte Logik der Sitzungserstellung wird von unserem Server kommen. Darauf kommen wir später noch zu sprechen.

$(function() {
let client = ZAFClient.init();
client.invoke('resize', { width: '100%', height: '79vh'  });
videos.style.display = 'none';

client.get(['ticket.id', 'ticket.requester.id']).then(data => {
let user_id = data['ticket.requester.id']
let  ticket_id = data['ticket.id'];

    fetch(SERVER_BASE_URL + '/room/' + user_id + "-" + ticket_id).then(res => {
    return res.json()
      }).then(res => {
        apiKey = res.apiKey;
        sessionId = res.sessionId;
        token = res.token;
      }).catch(handleError);

  });

});

Nun, da wir diese Werte haben, können wir den Agenten entscheiden lassen, wann die Videositzung eingeleitet werden soll. Wir definieren eine initializeSession Funktion, die ausgelöst wird, sobald der Agent auf die Schaltfläche Initiate session Schaltfläche klickt. Wir setzen die Anzeige des Publisher-Containers auf Block, um ihn sichtbar zu machen (da er anfangs auf none gesetzt ist). Wir starten die Sitzung, indem wir ein Sitzungsobjekt instanziieren, und initialisieren dann den Herausgeber.

let initializeSession = () => {
  session = OT.initSession(apiKey, sessionId);

  // Create a publisher
  publisher = OT.initPublisher('publisher', {
    insertMode: 'replace',
    publishVideo: false,
  }, handleError);

  // Connect to the session
  session.connect(token, error => {
    // If the connection is successful, initialize a publisher and publish to the session
    if (error) {
      handleError(error);
    } else {
    session.publish(publisher)
    document.getElementById("initiatesession").style.display = "none"
    }
  });
}

Wir werden auch einige Hörer für Ereignisse die durch das Sitzungsobjekt ausgelöst werden. Wir nutzen die archiveStarted und archiveSopped nutzen, um den Zustand unserer Anwendung zu steuern, d. h. um zu wissen, ob wir das Video veröffentlichen oder ob es ausgeschaltet ist, wenn wir es aufzeichnen.

Je nach Zustand wird in den HTML-Schaltflächen ein anderer Wert angezeigt. Wenn wir zum Beispiel die Meldung archiveStartederhalten, soll die Schaltfläche "Archiv stoppen" und nicht "Archiv starten" lauten, da die Archivierung/Aufnahme bereits eingeleitet wurde. Am Anfang unseres Codes haben wir einige Zustandsvariablen definiert (archiving, video, und screen) definiert, die sich in Abhängigkeit von diesen Ereignissen ändern werden.

Wir werden auch einen Stream abonnieren wollen, sobald er erstellt ist, also werden wir auf das streamCreated Ereignis.

session.on('archiveStarted', event => {
            archiveID = event.id;
            archiving = true
            document.getElementById('handleRecording').innerHTML = 'Stop Archive';
            console.log('ARCHIVE STARTED ' + archiveID);
  });  

session.on('archiveStopped',  event => {
            archiveID = event.id;
            archiving = false
            document.getElementById('handleRecording').innerHTML = 'Start Archive';
            console.log('ARCHIVE STOPED ' + archiveID);
  });  

session.on("streamPropertyChanged", event => {
             video = event.newValue
             video ? document.getElementById("startPublishingVideoId").innerHTML = 'Turn Video off' : document.getElementById("startPublishingVideoId").innerHTML = 'Turn on Video';
            });

  session.on('streamCreated', event => {
    console.log('stream created' + event.stream)
    session.subscribe(event.stream, 'subscriber', {
      insertMode: 'append',

    }, handleError);
  });

Die Funktion handleError Funktion, die wir als Callback übergeben, ist eine Funktion, die eine Warnung auslöst, wenn beim Abhören von Ereignissen in der Sitzung ein Fehler auftritt.

let handleError = (error) => {
  if (error) {
    alert(error.message);
  }
}

Wir können eine handleRecording Funktion erstellen, die feststellt, ob wir bereits eine Aufzeichnung durchführen oder nicht. So können wir je nach Zustand eine andere Funktion auslösen.

let handleRecording = () => {
  archiving ? stopArchive() : startArchive();
}

Die Funktion StartArchive Funktion stellt eine POST-Anfrage an die Route unseres Servers archive/start Route. Wir müssen unsere sessionId übergeben, damit unser Server weiß, welche Sitzung die Aufzeichnung auslöst. Sie werden später in diesem Tutorial sehen, dass wir uns auf die Aufzeichnung und Speicherung der Sitzung beziehen. Lassen Sie sich nicht verwirren; es handelt sich um dasselbe Konzept, aber wir verwenden intern den Begriff "Archiv" :)

let startArchive = () => {
  console.log('start');
  fetch(SERVER_BASE_URL +'/archive/start', {
    method: 'post',
    headers: {
      'Content-type': 'application/json'
    },
    body: JSON.stringify({
      'sessionId': sessionId
    })
  })
  .then((response) => {
    return response.json();
  })
  .then((data) => {
    console.log('data from server when starting archiving', data)
  })
  .catch(error => console.log('errror starting archive', error))
}

Was die StopArchive Funktion ist es so ziemlich das Gleiche wie StartArchive. Aber in diesem Fall müssen wir die archiveID übergeben, die aus dem archiveStarted Ereignis stammt.

let stopArchive = () => {
  console.log('archiveID' + archiveID);
  fetch(SERVER_BASE_URL + '/archive/' + archiveID + '/stop', {
    method: 'post',
    headers: {
      'Content-type': 'application/json'
    }
  })
  .then((response) => {
    return response.json()
  })
  .then((data) => {
    console.log('data from server when stopping archiving', data)
  })
  .catch(error => console.log('errror stopping archive', error))
}

Jetzt müssen wir die Unterstützung für Screen-Sharing-Streams hinzufügen. Wir werden eine Funktion erstellen, die prüft, ob wir unseren Bildschirm bereits freigeben, und wenn nicht, wird sie einen neuen Herausgeber erstellen. Diese Funktion wird als Umschalter für den Bildschirmfreigabe-Stream in Verbindung mit einigen Ereignissen fungieren, genau wie wir es bei der Archivierung gemacht haben.

Wir prüfen, ob der Browser Screen Sharing unterstützt, indem wir die OT.checkScreenSharingCapability Methode. Wir erklären mehr über die Unterstützung von Screen Sharing in der Dokumentation zum checkScreenSharingCapability Callback. Für einige ältere Browserversionen müssen Sie möglicherweise eine Erweiterung installieren, aber der Einfachheit halber gehen wir davon aus, dass beide Teilnehmer einen aktuellen Browser verwenden.

Beachten Sie, dass die Ereignisse, auf die wir in diesem Fall hören, vom Publisher-Objekt und nicht vom Session-Objekt versendet werden. Siehe die StreamEvent für weitere Informationen.

const startPublishingScreen = () => {
  if (screenSharing === true) {
    session.unpublish(screenPublisher)
  } else {
    OT.checkScreenSharingCapability(response => {
      if (!response.supported || response.extensionRegistered === false) {
        alert('Screen share is not supported in this browser')
      } else {
        screenPublisher = OT.initPublisher('screen', {
          videoSource: 'screen'
        }, error => {
          if (error) {
            console.log(error)
          } else {
            session.publish(screenPublisher, handleError)
              .on("streamCreated", event => {
                if (event.stream.videoType === 'screen') {
                  screenSharing = true;
                  document.getElementById("startPublishingScreenId").innerHTML = 'stop screenShare'
                }
              })
              .on("streamDestroyed", event => {
                if (event.stream.videoType === 'screen') {
                  screenSharing = false
                  document.getElementById("startPublishingScreenId").innerHTML = 'start screenShare'
                }
              })
          }
        })
      }

    })
  }
}

Kunden-Seite

Nachdem wir nun unsere Agentenseite eingerichtet haben, müssen wir darüber nachdenken, wie wir Video auf der Kundenseite hinzufügen können. Der Hauptzweck dieses Beitrags besteht darin, die Verbindung zwischen dem Endkunden (Ticketanforderer) und dem Supportmitarbeiter (Ticketzuweiser) herzustellen.

Um das zu tun, folgen wir der Anleitung zum Anpassen des Help-Center-Themas so dass wir Zugriff auf den Seitencode des Ticketanforderers erhalten und ein besseres Kundenerlebnis im Help Center schaffen können.

In diesem Fall sind wir an der Anpassung der Requests pageDas heißt, die Listen der Anfragen oder Tickets, die einem bestimmten Benutzer zugewiesen sind. Wie in dem oben verlinkten Artikel erläutert, ist das HTML für das Help Center in bearbeitbaren Vorlagen enthalten. Wir werden die Datei requests_page.hbs Datei bearbeiten. Der Code wird dem JavaScript-Code in der Datei main.js Datei.

Als Erstes importieren wir die Opentok-Bibliothek. Dadurch wird die neueste Version des JS SDK heruntergeladen.

<script src="https://static.opentok.com/v2/js/opentok.min.js"></script>

Wir fügen einige grundlegende Markups hinzu, die das Video des Herausgebers und des Abonnenten sowie einige Schaltflächen enthalten, die die Funktionalität unserer Anwendung steuern werden. Sie haben sicher bemerkt, dass wir {{assignee.avatar_url}}. Das ist eine Vorlagensprache namens Curlybars die es uns ermöglicht, mit Help Center Daten im Kontext von Zendsk Tickets zu interagieren.

In diesem Beispiel wird auf der Schaltfläche, die den Videoanruf einleitet, ein Bild des Ticketinhabers angezeigt. Das Ziel ist es, dem Kunden ein nahes Erlebnis zu bieten. Der Einfachheit halber blenden wir zunächst alle Schaltflächen aus, mit Ausnahme der Schaltfläche, die den Anruf einleitet. Dazu setzen wir die Eigenschaft display unserer HTML-Elemente auf none.

<div>
<button class="button" onclick="initializeSession()" style="position:relative"> 
  <img src={{assignee.avatar_url}} />
  <span class="tooltiptext">Discuss live with {{assignee.name}}</span>
</button>
</div>
    
    <button id="startPublishingVideoId" class="button" onclick="toggleVideo()" style="display:none">Turn Video off</button>
    
    <button id="handleRecording" class="button" onclick="handleRecording()" style="display:none>Start video recording</button>
    
    <button id="startPublishingScreenId" class="button" onclick="startPublishingScreen()" style="display:none">Share your screen</button>

<div id="videos">
    <div id="publisher"></div>
    <div id="subscriber"></div>
</div>

Wir werden einige Variablen definieren, die wir im gesamten Code verwenden werden. Wie auf der Agentenseite werden wir mit einigen Zustandsvariablen arbeiten (video, archiving, und screenSharing). Wir werden auch den Endpunkt unseres Servers definieren.

let sessionId;
let publisher;
let archiveId;
let screenSharing = false;
let archiving = false;
let video = true;
const SERVER_BASE_URL = 'SERVER_BASE_URL';

Wir definieren eine einfache Fehlerbehandlungsfunktion, mit der wir den Benutzer im Falle eines Fehlers benachrichtigen wollen. Das einzige Ziel, das wir mit der Definition dieser Funktion verfolgen, ist es, unseren Code ein wenig aufzuräumen.

const handleError = (error) => {
  if (error) {
    alert(error.message);
  }
}

Wir holen uns apiKey, sessionId, und token von unserem Server ab.

fetch(SERVER_BASE_URL + '/room/' + {{request.requester.id}} + '-' +{{request.id}}).then(res => {
  return res.json()
}).then(res => {
  apiKey = res.apiKey;
  sessionId = res.sessionId;
  token = res.token;
}).catch(handleError);

Fügen Sie dann die folgende initializeSession Funktion hinzu, die ausgelöst wird, sobald der Kunde beschließt, ein Video-Gespräch mit dem Support-Agenten anzufordern. Wir zeigen die Schaltflächen an, die zunächst ausgeblendet waren, dann instanziieren wir zunächst ein Sitzungsobjekt und erstellen einen Herausgeber. Zum Schluss versuchen wir, eine Verbindung zur Sitzung herzustellen. Wenn die Verbindung erfolgreich ist, werden wir versuchen, die Sitzung zu veröffentlichen, wie zuvor erklärt.

const initializeSession = () => {
  document.getElementById('startPublishingVideoId').style.display = "block";
  document.getElementById('handleRecording').style.display = "block";
  document.getElementById('startPublishingScreenId').style.display = "block";
  videos.style.display = 'block';
  session = OT.initSession(apiKey, sessionId);

  publisher = OT.initPublisher('publisher', {
    insertMode: 'append',
    width: '100%',
    height: '100%',
  }, handleError);

  session.connect(token, error => {

    if (error) {
      handleError(error);
    } else {
      session.publish(publisher, handleError);
    }
  });

  session.on('streamCreated', (event) => {
    session.subscribe(event.stream, 'subscriber', {
      insertMode: 'append',
      width: '100%',
      height: '100%'
    }, handleError);
  });

  session.on('archiveStarted', event => {
    archiveID = event.id;
    archiving = true
    document.getElementById('handleRecording').innerHTML = 'Stop Archive';
    console.log('ARCHIVE STARTED ' + archiveID);
  });

  session.on('archiveStopped', event => {
    archiveID = event.id;
    archiving = false
    document.getElementById('handleRecording').innerHTML = 'Start Archive';
    console.log('ARCHIVE STOPED ' + archiveID);
  });

  session.on("streamPropertyChanged", event => {
    console.log(event.newValue)
    video = event.newValue
    video ? document.getElementById("startPublishingVideoId").innerHTML = 'Turn Video off' : document.getElementById("startPublishingVideoId").innerHTML = 'Turn Video on';
  });

  session.on('streamCreated', event => {
    session.subscribe(event.stream, 'subscriber', {
      insertMode: 'append',
    }, handleError);
  });
}

Wir werden ternäre Operatoren verwenden, um zu entscheiden, ob wir das Video ein- oder ausschalten müssen. Dieselbe Logik gilt für die Entscheidung, ob die Funktion zum Starten oder Stoppen der Aufnahme aufgerufen werden soll.

const toggleVideo = () => {
video ? publisher.publishVideo(false) : publisher.publishVideo(true)
}

const handleRecording = () => {
  archiving ? stopArchive() : startArchive();
}

Die startArchive() und startArchive() sehen genau so aus wie die Funktionen in main.jsalso lassen wir sie der Einfachheit halber weg. Vielleicht möchten Sie auch nur dem Support-Mitarbeiter und nicht dem Endkunden die Möglichkeit geben, Aufzeichnungen zu starten, aber das ist ganz Ihnen überlassen. Damit es mehr Spaß macht, erlauben wir beiden, Aufnahmen zu starten und zu stoppen, da beide die Möglichkeit haben, die Aufzeichnung nach dem Anruf abzurufen.

Server

Unsere Serverseite wird aus mehreren Routen bestehen, um die Anfragen zu bearbeiten, die entweder vom Agenten oder vom Supporttechniker kommen.

Lassen Sie uns die Module importieren, die wir für unsere Anwendung verwenden werden, und einige Umgebungsvariablen definieren.

apiKey und apiSecret sind die Video API-Anmeldeinformationen, die Sie in Ihrem DashboardDie remoteUri verweist auf den Zendesk-Endpunkt Ihrer Organisation in Form von https://xxxxxx.zendesk.com/. Für die Zendesk-Authentifizierung lesen Sie bitte die Seite "Wie kann ich API-Anfragen authentifizieren?", da sie verschiedene Authentifizierungsmethoden unterstützen; wir haben Benutzernamen und Token verwendet.

Was die Authentifizierung bei AWS betrifft, so gibt es mehrere unterstützte MethodenWir haben uns aber auch für Umgebungsvariablen entschieden. Beachten Sie, dass in diesem Fall das SDK automatisch die AWS-Anmeldeinformationen erkennt, die als Variablen in Ihrer Umgebung festgelegt sind, und sie für SDK-Anforderungen verwendet, wodurch die Notwendigkeit entfällt, Anmeldeinformationen in Ihrer Anwendung zu verwalten. Aus diesem Grund lesen wir die Variablen nicht aus unserer .env Datei.

const fs = require('fs');
const bodyParser = require('body-parser')
const express = require('express');
const path = require('path');
const app = express();
const _ = require('lodash');
const request = require ('request')
const ZD = require('node-zendesk');
const cors = require('cors');
const dotenv = require('dotenv')

dotenv.config();

const apiKey = process.env.apiKey
const  apiSecret = process.env.apiSecret
const AWS = require('aws-sdk');
const remoteUri = process.env.remoteUri

const client = ZD.createClient({
  username:  process.env.username,
  token:     process.env.token,
  remoteUri: process.env.remoteUri
});

const OpenTok = require('opentok');
const opentok = new OpenTok(apiKey, apiSecret);
app.use(cors());
app.use(bodyParser.json()); 
app.use(bodyParser.urlencoded({
  extended: true
}));
let ticketId
const app = express()
init()

Fügen Sie dies zu Ihrer index.js Datei hinzu.

const init = () => {
app.listen(8080,  () => {
console.log('You\'re app is now ready at http://localhost:8080/');
}

Die Route, die für die Erstellung von Sitzungen und Token zuständig ist, prüft, ob es bereits eine Sitzung zu diesem Ticket gibt, und wenn nicht, wird sie eine erstellen. Falls Sie nicht mit dem Konzept von Token für die Video API nicht vertraut sind, handelt es sich dabei um einen Schlüssel für den Raum (Sitzung).

Sie würden sich eine sicherere Lösung wünschen, aber wir haben uns der Einfachheit halber für eine einfache Validierung entschieden. In diesem Fall erhalten wir einen name Parameter in folgendem Format XXXXXX-YYYYY. Erinnern Sie sich an die Abrufe, die wir in beiden Teilen (Agent und Kunde) gemacht haben? Sie kommen von dort.

Wir erzeugen nur dann eine Sitzung und ein Token, wenn die Antragsteller-ID des Tickets mit dem zweiten Teil unseres :name Parameters übereinstimmt. Wir werden ein Zendesk-Paket verwenden, um die Validierung durchzuführen. Ein Beispiel: Wenn wir 1222-1234erhalten, überprüfen wir über die Zendesk-API, ob das Ticket 1234 tatsächlich von Benutzer 1222 angefordert wurde. Wenn nicht, wird ein HTTP 404 zurückgegeben.

Sie werden auch sehen, dass der Referer und der Ursprung der Anfrage überprüft werden. Das ist ein schneller Hack, um das Ticket nur dann zu aktualisieren, wenn die Anfrage vom Kunden kommt, und um den Support-Techniker wissen zu lassen, dass der Anfragende eine Video-Sitzung haben möchte.

app.get('/room/:name', (req, res) => {
  if (!req.params.name) {
    res.status(402).end()
  }
  let roomName = req.params.name;
  let sessionId;
  let requesterId = roomName.split("-")[0]
  ticketId = roomName.split("-")[1]

  checkIfValid(ticketId, req).then(response => {

      if (response && response.toString() === requesterId) {

        if (req.headers.origin === endpoint && req.headers.referer.split("/")[3] === "hc") {
          updateTicket(ticketId)
        }

        if (roomToSessionIdDictionary[roomName]) {
          sessionId = roomToSessionIdDictionary[roomName];
          token = opentok.generateToken(sessionId);
          res.setHeader('Content-Type', 'application/json');
          res.send({
            apiKey: apiKey,
            sessionId: sessionId,
            token: token
          });
        } else {
          giveMeSession().then(session => {
              roomToSessionIdDictionary[roomName] = session.sessionId;
              token = opentok.generateToken(session.sessionId);
              res.setHeader('Content-Type', 'application/json');
              res.send({
                apiKey: apiKey,
                sessionId: session.sessionId,
                token: token
              });

            })
            .catch(e => res.status(500).send({
              error: 'createSession error:' + e
            }))
        }
      } else {
        res.status(404).end()
      }
    })
    .catch((e) => {
      res.status(404).end()
    })

})

In einer realen Anwendung müssten Sie wahrscheinlich die Sitzungs-IDs in Ihrer Datenbank speichern und prüfen, ob für dieses Ticket bereits eine Sitzung erstellt wurde. Für dieses Tutorial haben wir uns jedoch für die Verwendung eines Wörterbuchs entschieden, in dem Sitzungs-IDs in Verbindung mit einem Raumnamen gespeichert werden. Denken Sie daran, dass dieses Wörterbuch zurückgesetzt wird, sobald Sie Ihren Server neu starten.

let roomToSessionIdDictionary = {};

// returns the room name, given a session ID that was associated with it
const findRoomFromSessionId = sessionId => {
  return _.findKey(roomToSessionIdDictionary,  value => { return value === sessionId; });
}

Wie bereits erwähnt, wird eine Sitzung nur dann erstellt, wenn dem empfangenen Raumnamen keine Sitzung zugeordnet ist. Wir verpacken die callback-basierte Methode in ein Versprechen, das ein Sitzungsobjekt zurückgibt.

const giveMeSession = ()=>{
  return new Promise((resolve, reject) => {
        opentok.createSession({ mediaMode: 'routed' }, (err, session) => {
          if (err) {
            console.log('[Opentok - createRoutedSession] - Err', err);
            reject(err);
          }
          resolve(session);
        });
      })
    }

Wir haben auch die Zendesk-Prüfung in ein Versprechen verpackt, das uns erlaubt, die Ticket-ID abzufragen, die wir erhalten haben, damit wir feststellen können, ob die Anfrage legitim ist oder nicht.

const checkIfValid = (ticketId, res) => {
  return new Promise(
    (resolve, reject) => {
      client.tickets.show(ticketId, function(err, request, result){
        if (err) reject(err);
        resolve(result.requester_id);

      })
   }
 );
};

Wenn die Anfrage gültig ist und von der Kundenseite (nicht vom Agenten) kommt, aktualisieren Sie das Ticket so, dass der Support-Techniker darüber informiert wird, dass jemand auf eine Video-Sitzung wartet.

const updateTicket = (ticketId) => {
let notification  = 'The requester of the ticket would like to talk to you.'
 client.tickets.update(ticketId, {"ticket":{comment:{"body": notification, "public": false}}}, (err, req, res) => {
  if(!err){console.log('Ticket updated')                  
  }}
)}

Wir definieren die Routen zum Starten und Stoppen der Archivierung. Beachten Sie, dass die Route zum Beenden der Archivierung auch die Sitzungs-ID benötigt. So wissen unsere Server, für welche Sitzungs-ID Sie die Aufzeichnung stoppen wollen.

app.post('/archive/start',  (req, res) => {
  var json = req.body;
  var sessionId = json.sessionId;
  opentok.startArchive(sessionId, { name: 'testSession' },  (err, archive) => {
    if (err) {
      console.error(err);
      res.status(500).send({ error: 'startArchive error:' + err });
      return;
    }
    res.setHeader('Content-Type', 'application/json');
    res.send(archive);
  });
});

app.post('/archive/:archiveId/stop',  (req, res) => {
  opentok.stopArchive(archiveId, function (err, archive) {
    if (err) {
      console.error('error in stopArchive');
      console.error(err);
      res.status(500).send({ error: 'stopArchive error:' + err });
      return;
    }
    res.setHeader('Content-Type', 'application/json');
    res.send(archive);
  });
});

Wenn Sie Ihren Server betreiben, stellen Sie ihn mit ngrokund konfigurieren Sie die ngrok-URL als SERVER_BASE_URL in beiden Frontends (Kunden- und Agentenseite). Sie haben jetzt eine Video-Sitzung, gut gemacht!

Okay, das war cool, aber lassen Sie uns noch einen Schritt weiter gehen! Wäre es nicht toll, wenn wir die Aufzeichnung des Anrufs auch dynamisch verarbeiten und in Zendesk hochladen könnten, damit sowohl der Supporttechniker als auch der Kunde sie abrufen können, wenn es ihnen am besten passt? Das sollten wir tun!

Excited

Umgang mit Aufzeichnungen

Zunächst müssen wir der Video API mitteilen, wohin wir unsere Videoaufzeichnung hochladen wollen. Da wir einen AWS S3-Endpunkt verwenden werden, können Sie unsere Verwendung von S3-Speicher mit Vonage Video API-Archivierung Anleitung folgen. Sobald Sie eine Videositzung konfiguriert haben und eine Aufzeichnung starten und beenden, wird diese automatisch in Ihren S3-Bucket hochgeladen.

Alle Archive werden in einem Unterverzeichnis Ihres S3-Buckets gespeichert, das Ihren OpenTok-API-Schlüssel als Namen trägt, und jedes Archiv wird in einem Unterverzeichnis dieses Verzeichnisses gespeichert, das die Archiv-ID als Namen trägt. Die Archivdatei lautet archive.mp4.

Nehmen wir zum Beispiel ein Archiv mit dem folgenden API-Schlüssel und der folgenden ID:

  • API-Schlüssel: 123456

  • Archiv-ID: ab0baa3d-2539-43a6-be42-b41ff1488af3

Die Datei für dieses Archiv wird in das folgende Verzeichnis Ihres S3-Buckets hochgeladen:

123456/ab0baa3d-2539-43a6-be42-b41ff1488af3/archive.mp4

Als nächstes müssen wir wissen, wann das Archiv in unser S3-Bucket hochgeladen wurde, damit wir es abrufen können. Wir werden eine Route in unserem Server konfigurieren, um auf archivbezogene Ereignisse zu warten. Die Video API-Plattform sendet Ihnen einen Webhook an die zuvor konfigurierte Callback-URL, wenn sich der Status eines Archivs ändert.

Gehen Sie zu Ihrem Dashboard, klicken Sie auf das Projekt, das Sie verwenden, und konfigurieren Sie Ihre Server-URL auf https://YOUR_SERVER_URL/events. Wie in der Archivierungsanleitungerläutert, sendet Ihnen die Video API-Plattform einen Verfügbarkeitsstatus, sobald das Archiv zum Download aus dem S3-Bucket bereitsteht. Wir hören auf dieses Ereignis auf unserem Server und laden es herunter. Die gesamte Logik wird auf der Serverseite gehandhabt (server.js Datei).

app.post('/events',  (req, res) => {
  res.send('OK')
  if(req.body.status === 'uploaded'){
  let key = apiKey + "/" + req.body.id + "/archive.mp4"
  downloadVideo(req.body.id + ".mp4", key)
  }
})

Denken Sie daran, Ihre Server-URL in Ihrem Video API Account zu konfigurieren. Andernfalls werden Sie diese Webhooks nicht auf Ihrem Server empfangen. Sie sollte etwa so aussehen wie die folgende:

Callback URL

Wir übergeben zwei Variablen an die downloadVideo eine ist der Name, unter dem unser Archiv heruntergeladen werden soll, und die andere ist der Schlüssel, damit unser S3-Bucket weiß, welche Aufnahme wir abrufen wollen.

Die Anfrage streamt die zurückgegebenen Daten direkt in ein Node.js Stream-Objekt, indem sie die createReadStream Methode auf die Anfrage. Der Aufruf von createReadStream gibt den von der Anfrage verwalteten HTTP-Rohdatenstrom zurück. Der Rohdatenstrom kann dann in ein Node.js Stream-Objekt weitergeleitet werden. Wir sollten nun in der Lage sein, die Aufnahmen dynamisch herunterzuladen, sobald sie in unseren Bucket hochgeladen wurden.

const downloadVideo = (name, key) => {
  var fileStream = fs.createWriteStream(name);
  s3 = new AWS.S3();
  var s3Stream = s3.getObject({Bucket: process.env.BucketName, Key: key}).createReadStream();
  s3Stream.on('error', (err) => {
  console.error(err);
  });

  s3Stream.pipe(fileStream).on('error', (err) => {
      // capture any errors that occur when writing data to the file
      console.error('File Stream:', err);
  }).on('close', () => {
      console.log('Done.');
      getToken(name)
  });
}

Sie werden bemerkt haben, dass wir eine getToken Funktion aufrufen, sobald das Herunterladen der Datei abgeschlossen ist. Das liegt an dem Prozess des Hochladens einer Datei in Zendesk. Sie können zu diesem Zeitpunkt mit der Datei machen, was Sie wollen, da sie bereits heruntergeladen ist. Um unseren Beitrag abzuschließen, laden wir die Aufzeichnung jedoch in das Zendesk-Ticket hoch, damit beide Teilnehmer die Aufzeichnung nach dem Gespräch ansehen können.

Zuerst müssen wir ein Token erhalten, und dann müssen wir das Ticket mit diesem Token aktualisieren. Den zweiten Teil erledigen wir in einer separaten Funktion namens uploadVideo.

const getToken = (archiveName) => {
  client.attachments.upload(__dirname + '/' + archiveName , {binary: false, filename: archiveName}, (err, req, result) => {
    if (err) {
      console.log("error:", err);
    }
    console.log("token:", result.upload.token);
    uploadVideo(result.upload.token, ticketId)
  })
}
const uploadVideo = (token, ticketId) =>{
  let ticket = {
  "ticket":{"comment": { "body": "This is the recording of the call", "public": true, "uploads":[token]},
  }};
  client.tickets.update(ticketId,ticket, (err, req, res) => {
    if(!err){
      console.log('ticket updated with the video recording')
    }
  })
}

Sehen Sie sich die Demo um eine bessere Vorstellung davon zu bekommen, wie das alles funktioniert. Passen Sie dieses Tutorial an Ihre Bedürfnisse an, damit Ihre Kunden hochzufrieden sind und sich als echte Befürworter des Supports fühlen.

Sie finden den Code für dieses Projekt im Verzeichnis vonage-zendesk-Integration GitHub-Repositorium.

Was werden Sie als nächstes bauen? Lassen Sie es uns wissen!

Teilen Sie:

https://a.storyblok.com/f/270183/384x384/6007824739/javier-molina-sanz.png
Javier Molina Sanz

Javier studied Industrial Engineering back in Madrid where he's from. He is now one of our Solution Engineers, so if you get into trouble using our APIs he may be the one that gives you a hand. Out of work he loves playing football and travelling as much as he can.