Поделиться через


Быстрый старт: Подключите своё приложение для чата к собранию в Teams

Приступите к работе со Службами коммуникации Azure, подключив решение для чата к Microsoft Teams.

В этой статье описывается, как общаться в собрании Teams с помощью пакета SDK чата Служб коммуникации Azure для JavaScript.

Пример кода

Скачайте этот код на сайте GitHub Azure Samples Подключите ваше приложение для чата к собранию в Teams.

Предварительные условия

Присоединение к чату собрания

Пользователь Служб коммуникации может присоединиться к собранию Teams в качестве анонимного пользователя с помощью пакета SDK для вызова. Присоединение к собранию также добавляет их в чат собрания, где они могут отправлять и получать сообщения с другими пользователями в собрании. У пользователя нет доступа к сообщениям чата, отправленным до присоединения к собранию, и после окончания собрания они не могут отправлять или получать сообщения. Чтобы присоединиться к собранию и начать беседу, можно выполнить следующие действия.

Создание нового приложения Node.js

Откройте терминал или командное окно, создайте каталог для своего приложения и перейдите к нему.

mkdir chat-interop-quickstart && cd chat-interop-quickstart

Воспользуйтесь командой npm init -y, чтобы создать файл package.json с параметрами по умолчанию.

npm init -y

Установка пакетов чата

Используйте команду npm install, чтобы установить необходимые пакеты SDK Служб коммуникации для JavaScript.

npm install @azure/communication-common --save

npm install @azure/communication-identity --save

npm install @azure/communication-chat --save

npm install @azure/communication-calling --save

Параметр --save указывает библиотеку как зависимость в файле пакета package.json.

Настройка платформы приложения

В этом примере используется веб-пакет для упаковки ресурсов приложения. Выполните следующую команду, чтобы установить пакеты npm webpack, webpack-cli и webpack-dev-server и перечислить их в качестве зависимостей разработки в package.json:

npm install [email protected] [email protected] [email protected] --save-dev

Создайте файл index.html в корневом каталоге проекта. Этот файл используется для настройки базового макета, позволяющего пользователю присоединиться к собранию и начать чат.

Добавление элементов управления пользовательского интерфейса Teams

Замените код в файле index.html приведенным ниже фрагментом кода.

Используйте текстовое поле в верхней части страницы, чтобы ввести контекст встречи в Teams. Конечные пользователи могут присоединиться к указанному собранию, нажав кнопку "Присоединиться к собранию Teams ".

Всплывающее окно чата отображается в нижней части страницы. Конечные пользователи могут использовать его для отправки сообщений по теме собрания. Он отображает в режиме реального времени любые сообщения, отправленные в теме, пока пользователь служб связи является её участником.

<!DOCTYPE html>
<html>
   <head>
      <title>Communication Client - Calling and Chat Sample</title>
      <style>
         body {box-sizing: border-box;}
         /* The popup chat - hidden by default */
         .chat-popup {
         display: none;
         position: fixed;
         bottom: 0;
         left: 15px;
         border: 3px solid #f1f1f1;
         z-index: 9;
         }
         .message-box {
         display: none;
         position: fixed;
         bottom: 0;
         left: 15px;
         border: 3px solid #FFFACD;
         z-index: 9;
         }
         .form-container {
         max-width: 300px;
         padding: 10px;
         background-color: white;
         }
         .form-container textarea {
         width: 90%;
         padding: 15px;
         margin: 5px 0 22px 0;
         border: none;
         background: #e1e1e1;
         resize: none;
         min-height: 50px;
         }
         .form-container .btn {
         background-color: #4CAF40;
         color: white;
         padding: 14px 18px;
         margin-bottom:10px;
         opacity: 0.6;
         border: none;
         cursor: pointer;
         width: 100%;
         }
         .container {
         border: 1px solid #dedede;
         background-color: #F1F1F1;
         border-radius: 3px;
         padding: 8px;
         margin: 8px 0;
         }
         .darker {
         border-color: #ccc;
         background-color: #ffdab9;
         margin-left: 25px;
         margin-right: 3px;
         }
         .lighter {
         margin-right: 20px;
         margin-left: 3px;
         }
         .container::after {
         content: "";
         clear: both;
         display: table;
         }
      </style>
   </head>
   <body>
      <h4>Azure Communication Services</h4>
      <h1>Calling and Chat Quickstart</h1>
          <input id="teams-link-input" type="text" placeholder="Teams meeting link"
        style="margin-bottom:1em; width: 400px;" />
        <p>Call state <span style="font-weight: bold" id="call-state">-</span></p>
      <div>
        <button id="join-meeting-button" type="button">
            Join Teams Meeting
        </button>
        <button id="hang-up-button" type="button" disabled="true">
            Hang Up
        </button>
      </div>
      <div class="chat-popup" id="chat-box">
         <div id="messages-container"></div>
         <form class="form-container">
            <textarea placeholder="Type message.." name="msg" id="message-box" required></textarea>
            <button type="button" class="btn" id="send-message">Send</button>
         </form>
      </div>
      <script src="./bundle.js"></script>
   </body>
</html>

Включение элементов управления пользовательского интерфейса Teams

Замените содержимое файла client.js приведенным ниже фрагментом кода.

Во фрагменте кода замените

  • SECRET_CONNECTION_STRING со строкой подключения вашей Службы коммуникации
import { CallClient } from "@azure/communication-calling";
import { AzureCommunicationTokenCredential } from "@azure/communication-common";
import { CommunicationIdentityClient } from "@azure/communication-identity";
import { ChatClient } from "@azure/communication-chat";

let call;
let callAgent;
let chatClient;
let chatThreadClient;

const meetingLinkInput = document.getElementById("teams-link-input");
const callButton = document.getElementById("join-meeting-button");
const hangUpButton = document.getElementById("hang-up-button");
const callStateElement = document.getElementById("call-state");

const messagesContainer = document.getElementById("messages-container");
const chatBox = document.getElementById("chat-box");
const sendMessageButton = document.getElementById("send-message");
const messageBox = document.getElementById("message-box");

var userId = "";
var messages = "";
var chatThreadId = "";

async function init() {
  const connectionString = "<SECRET_CONNECTION_STRING>";
  const endpointUrl = connectionString.split(";")[0].replace("endpoint=", "");

  const identityClient = new CommunicationIdentityClient(connectionString);

  let identityResponse = await identityClient.createUser();
  userId = identityResponse.communicationUserId;
  console.log(`\nCreated an identity with ID: ${identityResponse.communicationUserId}`);

  let tokenResponse = await identityClient.getToken(identityResponse, ["voip", "chat"]);

  const { token, expiresOn } = tokenResponse;
  console.log(`\nIssued an access token that expires at: ${expiresOn}`);
  console.log(token);

  const callClient = new CallClient();
  const tokenCredential = new AzureCommunicationTokenCredential(token);

  callAgent = await callClient.createCallAgent(tokenCredential);
  callButton.disabled = false;
  chatClient = new ChatClient(endpointUrl, new AzureCommunicationTokenCredential(token));

  console.log("Azure Communication Chat client created!");
}

init();

const joinCall = (urlString, callAgent) => {
  const url = new URL(urlString);
  console.log(url);
  if (url.pathname.startsWith("/meet")) {
    // Short teams URL, so for now call meetingID and pass code API
    return callAgent.join({
      meetingId: url.pathname.split("/").pop(),
      passcode: url.searchParams.get("p"),
    });
  } else {
    return callAgent.join({ meetingLink: urlString }, {});
  }
};

callButton.addEventListener("click", async () => {
  // join with meeting link
  try {
    call = joinCall(meetingLinkInput.value, callAgent);
  } catch {
    throw new Error("Could not join meeting - have you set your connection string?");
  }

  // Chat thread ID is provided from the call info, after connection.
  call.on("stateChanged", async () => {
    callStateElement.innerText = call.state;

    if (call.state === "Connected" && !chatThreadClient) {
      chatThreadId = call.info?.threadId;
      chatThreadClient = chatClient.getChatThreadClient(chatThreadId);

      chatBox.style.display = "block";
      messagesContainer.innerHTML = messages;

      // open notifications channel
      await chatClient.startRealtimeNotifications();

      // subscribe to new message notifications
      chatClient.on("chatMessageReceived", (e) => {
        console.log("Notification chatMessageReceived!");

        // check whether the notification is intended for the current thread
        if (chatThreadId != e.threadId) {
          return;
        }

        if (e.sender.communicationUserId != userId) {
          renderReceivedMessage(e.message);
        } else {
          renderSentMessage(e.message);
        }
      });
    }
  });

  // toggle button and chat box states
  hangUpButton.disabled = false;
  callButton.disabled = true;

  console.log(call);
});

async function renderReceivedMessage(message) {
  messages += '<div class="container lighter">' + message + "</div>";
  messagesContainer.innerHTML = messages;
}

async function renderSentMessage(message) {
  messages += '<div class="container darker">' + message + "</div>";
  messagesContainer.innerHTML = messages;
}

hangUpButton.addEventListener("click", async () => {
  // end the current call
  await call.hangUp();
  // Stop notifications
  chatClient.stopRealtimeNotifications();

  // toggle button states
  hangUpButton.disabled = true;
  callButton.disabled = false;
  callStateElement.innerText = "-";

  // toggle chat states
  chatBox.style.display = "none";
  messages = "";
  // Remove local ref
  chatThreadClient = undefined;
});

sendMessageButton.addEventListener("click", async () => {
  let message = messageBox.value;

  let sendMessageRequest = { content: message };
  let sendMessageOptions = { senderDisplayName: "Jack" };
  let sendChatMessageResult = await chatThreadClient.sendMessage(
    sendMessageRequest,
    sendMessageOptions
  );
  let messageId = sendChatMessageResult.id;

  messageBox.value = "";
  console.log(`Message sent!, message id:${messageId}`);
});

Клиент Teams не задает отображаемые имена участников потока чата. Имена возвращаются в виде null в API для перечисления участников, в событии participantsAdded и в событии participantsRemoved. Отображаемые имена участников чата можно получить из поля remoteParticipants объекта call. При получении уведомления об изменении состава можно использовать этот код для получения имени пользователя, который был добавлен или удален:

var displayName = call.remoteParticipants.find(p => p.identifier.communicationUserId == '<REMOTE_USER_ID>').displayName;

Выполнение кода

Чтобы создать и запустить приложение, используйте webpack-dev-server. Выполните следующую команду, чтобы создать пакет вашего приложения на локальном веб-сервере.

npx webpack-dev-server --entry ./client.js --output bundle.js --debug --devtool inline-source-map

Откройте браузер и перейдите по адресу http://localhost:8080/. Приложение должно быть запущено, как показано на следующем снимке экрана:

Снимок экрана: готовое приложение JavaScript.

Вставьте ссылку на собрание Teams в текстовое поле. Пользователи могут нажать кнопку "Присоединиться к собранию Teams" , чтобы присоединиться к собранию Teams. После того как пользователь служб коммуникации будет принят на собрание, вы можете общаться в приложении Служб коммуникации. Чтобы начать беседу, перейдите к полю в нижней части страницы. Для простоты приложение отображает только последние два сообщения в чате.

Примечание.

Некоторые функции в настоящее время не поддерживаются для сценариев взаимодействия с Teams. Дополнительные сведения о поддерживаемых функциях см. в разделе "Возможности собраний Teams для внешних пользователей Teams".

В этой статье описывается, как общаться в собрании Teams с помощью пакета SDK чата служб коммуникации Azure для iOS.

Пример кода

Скачайте этот код на сайте GitHub Azure Samples. Присоедините своё приложение для чата к собранию в Teams.

Предварительные условия

az communication user-identity token issue --scope voip chat --connection-string "yourConnectionString"

Дополнительные сведения см. в статье "Создание маркеров доступа и управление ими" с помощью Azure CLI.

Установка

Создание проекта Xcode

В Xcode создайте новый проект iOS и выберите шаблон Single View App (Приложение с одним представлением). В этом руководстве используется платформа SwiftUI, поэтому для параметра Language (Язык) нужно задать значение Swift, а для параметра User Interface (Пользовательский интерфейс) — значение SwiftUI. В рамках этого краткого руководства вы не будете создавать тесты. Вы можете снять флажок Include Tests (Включить тесты).

Снимок экрана с окном New Project (Новый проект) в Xcode.

Установка CocoaPods

Используйте это руководство для установки CocoaPods на компьютере Mac.

Установка пакета и его зависимостей с помощью CocoaPods

  1. Чтобы создать файл Podfile для вашего приложения, откройте терминал, перейдите в папку вашего проекта и запустите pod init.

  2. Добавьте следующий код в Podfile под целевой объект и сохраните.

target 'Chat Teams Interop' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for Chat Teams Interop
  pod 'AzureCommunicationCalling'
  pod 'AzureCommunicationChat'
  
end
  1. Запустите pod install.

  2. .xcworkspace Откройте файл с помощью Xcode.

Запрос доступа к микрофону

Чтобы получить доступ к микрофону устройства, необходимо обновить список свойств приложения с помощью функции NSMicrophoneUsageDescription. Для параметра string , связанного с ним, включенного в диалоговое окно, система использует для запроса доступа от пользователя.

В целевом объекте выберите вкладку Info и добавьте строку для Privacy - Microphone Usage Description.

Снимок экрана: добавление использования микрофона в Xcode.

Отключение песочницы сценариев пользователя

Некоторые скрипты в связанных библиотеках записывают файлы во время сборки. Чтобы включить запись файлов, отключите песочницу пользовательского скрипта в Xcode.

В параметрах сборки найдите sandbox и установите значение User Script SandboxingNo.

Снимок экрана: отключение песочницы пользовательского скрипта в Xcode.

Присоединение к чату собрания

Пользователь Служб коммуникации может анонимно присоединиться к собранию Teams с помощью пакета SDK для вызовов. Когда пользователь присоединяется к собранию Teams, он может отправлять и получать сообщения с другими участниками собрания. Пользователь не имеет доступа к сообщениям чата, отправленным перед присоединением, и не может отправлять или получать сообщения, если они не в собрании.

Чтобы присоединиться к собранию и начать чат, выполните следующие действия.

Настройка платформы приложения

Импортируйте пакеты связи Azure в ContentView.swift, добавив следующий фрагмент кода:

import AVFoundation
import SwiftUI

import AzureCommunicationCalling
import AzureCommunicationChat

Добавьте следующий фрагмент кода в ContentView.swift, непосредственно выше декларации struct ContentView: View.

let endpoint = "<ADD_YOUR_ENDPOINT_URL_HERE>"
let token = "<ADD_YOUR_USER_TOKEN_HERE>"
let displayName: String = "Quickstart User"

Замените <ADD_YOUR_ENDPOINT_URL_HERE> значением конечной точки для ресурса Служб коммуникации. Замените <ADD_YOUR_USER_TOKEN_HERE> на ранее созданный маркер с помощью командной строки клиента Azure.

Дополнительные сведения см. в разделе "Маркер доступа пользователей"

Замените Quickstart User на отображаемое имя, которое хотите использовать в чате.

Чтобы сохранить состояние, добавьте в структуру ContentView следующие переменные:

  @State var message: String = ""
  @State var meetingLink: String = ""
  @State var chatThreadId: String = ""

  // Calling state
  @State var callClient: CallClient?
  @State var callObserver: CallDelegate?
  @State var callAgent: CallAgent?
  @State var call: Call?

  // Chat state
  @State var chatClient: ChatClient?
  @State var chatThreadClient: ChatThreadClient?
  @State var chatMessage: String = ""
  @State var meetingMessages: [MeetingMessage] = []

Теперь добавьте основную переменную для хранения элементов пользовательского интерфейса. Мы присоединяем бизнес-логику к этим элементам управления. Добавьте следующий код в структуру ContentView :

var body: some View {
    NavigationView {
      Form {
        Section {
          TextField("Teams Meeting URL", text: $meetingLink)
            .onChange(of: self.meetingLink, perform: { value in
              if let threadIdFromMeetingLink = getThreadId(from: value) {
                self.chatThreadId = threadIdFromMeetingLink
              }
            })
          TextField("Chat thread ID", text: $chatThreadId)
        }
        Section {
          HStack {
            Button(action: joinMeeting) {
              Text("Join Meeting")
            }.disabled(
              chatThreadId.isEmpty || callAgent == nil || call != nil
            )
            Spacer()
            Button(action: leaveMeeting) {
              Text("Leave Meeting")
            }.disabled(call == nil)
          }
          Text(message)
        }
        Section {
          ForEach(meetingMessages, id: \.id) { message in
            let currentUser: Bool = (message.displayName == displayName)
            let foregroundColor = currentUser ? Color.white : Color.black
            let background = currentUser ? Color.blue : Color(.systemGray6)
            let alignment = currentUser ? HorizontalAlignment.trailing : .leading
            
            HStack {
              if currentUser {
                Spacer()
              }
              VStack(alignment: alignment) {
                Text(message.displayName).font(Font.system(size: 10))
                Text(message.content)
                  .frame(maxWidth: 200)
              }

              .padding(8)
              .foregroundColor(foregroundColor)
              .background(background)
              .cornerRadius(8)

              if !currentUser {
                Spacer()
              }
            }
          }
          .frame(maxWidth: .infinity)
        }

        TextField("Enter your message...", text: $chatMessage)
        Button(action: sendMessage) {
          Text("Send Message")
        }.disabled(chatThreadClient == nil)
      }

      .navigationBarTitle("Teams Chat Interop")
    }

    .onAppear {
      // Handle initialization of the call and chat clients
    }
  }

Инициализируйте ChatClient

Создайте экземпляр ChatClient и включите уведомления о сообщениях. Мы используем уведомления в режиме реального времени для получения сообщений чата.

После установки основной части давайте добавим функции для настройки клиентов звонков и чата.

В функции onAppear добавьте следующий код, чтобы инициализировать CallClient и ChatClient:

  if let threadIdFromMeetingLink = getThreadId(from: self.meetingLink) {
    self.chatThreadId = threadIdFromMeetingLink
  }
  // Authenticate
  do {
    let credentials = try CommunicationTokenCredential(token: token)
    self.callClient = CallClient()
    self.callClient?.createCallAgent(
      userCredential: credentials
    ) { agent, error in
      if let e = error {
        self.message = "ERROR: It was not possible to create a call agent."
        print(e)
        return
      } else {
        self.callAgent = agent
      }
    }
  
    // Start the chat client
    self.chatClient = try ChatClient(
      endpoint: endpoint,
      credential: credentials,
      withOptions: AzureCommunicationChatClientOptions()
    )
    // Register for real-time notifications
    self.chatClient?.startRealTimeNotifications { result in
      switch result {
      case .success:
        self.chatClient?.register(
          event: .chatMessageReceived,
          handler: receiveMessage
      )
      case let .failure(error):
        self.message = "Could not register for message notifications: " + error.localizedDescription
        print(error)
      }
    }
  } catch {
    print(error)
    self.message = error.localizedDescription
  }

Добавление функции присоединения к собранию

Добавьте следующую функцию в ContentView структуру для обработки присоединения к собранию.

  func joinMeeting() {
    // Ask permissions
    AVAudioSession.sharedInstance().requestRecordPermission { (granted) in
      if granted {
        let teamsMeetingLink = TeamsMeetingLinkLocator(
          meetingLink: self.meetingLink
        )
        self.callAgent?.join(
          with: teamsMeetingLink,
          joinCallOptions: JoinCallOptions()
        ) {(call, error) in
          if let e = error {
            self.message = "Failed to join call: " + e.localizedDescription
            print(e.localizedDescription)
            return
          }

          self.call = call
          self.callObserver = CallObserver(self)
          self.call?.delegate = self.callObserver
          self.message = "Teams meeting joined successfully"
        }
      } else {
        self.message = "Not authorized to use mic"
      }
    }
  }

Инициализация ChatThreadClient

Мы инициализируем после ChatThreadClient присоединения пользователя к собранию. Затем необходимо проверить состояние собрания от делегата, а затем инициализировать ChatThreadClient при threadId присоединении к собранию.

Создайте функцию connectChat() со следующим кодом:

  func connectChat() {
    do {
      self.chatThreadClient = try chatClient?.createClient(
        forThread: self.chatThreadId
      )
      self.message = "Joined meeting chat successfully"
    } catch {
      self.message = "Failed to join the chat thread: " + error.localizedDescription
    }
  }

Добавьте следующую вспомогательную функцию в ContentView, используемую для разбора ID чата из ссылки на собрание команды, если это возможно. В случае сбоя извлечения пользователю необходимо вручную ввести идентификатор потока чата с помощью API Graph для получения идентификатора потока.

 func getThreadId(from teamsMeetingLink: String) -> String? {
  if let range = teamsMeetingLink.range(of: "meetup-join/") {
    let thread = teamsMeetingLink[range.upperBound...]
    if let endRange = thread.range(of: "/")?.lowerBound {
      return String(thread.prefix(upTo: endRange))
    }
  }
  return nil
}

Включение отправки сообщений

Добавьте функцию sendMessage() в ContentView. Эта функция использует ChatThreadClient, чтобы отправлять сообщения от пользователя.

func sendMessage() {
  let message = SendChatMessageRequest(
    content: self.chatMessage,
    senderDisplayName: displayName,
    type: .text
  )

  self.chatThreadClient?.send(message: message) { result, _ in
    switch result {
    case .success:
    print("Chat message sent")
    self.chatMessage = ""

    case let .failure(error):
    self.message = "Failed to send message: " + error.localizedDescription + "\n Has your token expired?"
    }
  }
}

Включение получения сообщений

Для получения сообщений мы реализуем обработчик событий ChatMessageReceived . Когда новые сообщения отправляются в поток, этот обработчик добавляет сообщения в meetingMessages переменную, чтобы они могли отображаться в пользовательском интерфейсе.

Сначала добавьте следующую структуру в ContentView.swift. Пользовательский интерфейс использует данные в структуре для отображения сообщений чата.

struct MeetingMessage: Identifiable {
  let id: String
  let date: Date
  let content: String
  let displayName: String

  static func fromTrouter(event: ChatMessageReceivedEvent) -> MeetingMessage {
    let displayName: String = event.senderDisplayName ?? "Unknown User"
    let content: String = event.message.replacingOccurrences(
      of: "<[^>]+>", with: "",
      options: String.CompareOptions.regularExpression
    )
    return MeetingMessage(
      id: event.id,
      date: event.createdOn?.value ?? Date(),
      content: content,
      displayName: displayName
    )
  }
}

Затем добавьте функцию receiveMessage() в ContentView. При возникновении события обмена сообщениями вызывает эту функцию. Для всех событий, которые вы хотите обрабатывать в инструкции switch, необходимо зарегистрироваться с помощью метода chatClient?.register().

  func receiveMessage(event: TrouterEvent) -> Void {
    switch event {
    case let .chatMessageReceivedEvent(messageEvent):
      let message = MeetingMessage.fromTrouter(event: messageEvent)
      self.meetingMessages.append(message)

      /// OTHER EVENTS
      //    case .realTimeNotificationConnected:
      //    case .realTimeNotificationDisconnected:
      //    case .typingIndicatorReceived(_):
      //    case .readReceiptReceived(_):
      //    case .chatMessageEdited(_):
      //    case .chatMessageDeleted(_):
      //    case .chatThreadCreated(_):
      //    case .chatThreadPropertiesUpdated(_):
      //    case .chatThreadDeleted(_):
      //    case .participantsAdded(_):
      //    case .participantsRemoved(_):

    default:
      break
    }
  }

Наконец, необходимо реализовать обработчик делегата для клиента вызова. Этот обработчик используется для проверки состояния вызова и инициализации клиента чата при присоединении пользователя к собранию.

class CallObserver : NSObject, CallDelegate {
  private var owner: ContentView

  init(_ view: ContentView) {
    owner = view
  }

  func call(
    _ call: Call,
    didChangeState args: PropertyChangedEventArgs
  ) {
    owner.message = CallObserver.callStateToString(state: call.state)
    if call.state == .disconnected {
      owner.call = nil
      owner.message = "Left Meeting"
    } else if call.state == .inLobby {
      owner.message = "Waiting in lobby (go let them in!)"
    } else if call.state == .connected {
      owner.message = "Connected"
      owner.connectChat()
    }
  }

  private static func callStateToString(state: CallState) -> String {
    switch state {
    case .connected: return "Connected"
    case .connecting: return "Connecting"
    case .disconnected: return "Disconnected"
    case .disconnecting: return "Disconnecting"
    case .earlyMedia: return "EarlyMedia"
    case .none: return "None"
    case .ringing: return "Ringing"
    case .inLobby: return "InLobby"
    default: return "Unknown"
    }
  }
}

Выход из чата

Когда пользователь покидает собрание Teams, мы очищаем сообщения чата из пользовательского интерфейса и зависаем от звонка. См. следующий полный пример кода.

  func leaveMeeting() {
    if let call = self.call {
      self.chatClient?.unregister(event: .chatMessageReceived)
      self.chatClient?.stopRealTimeNotifications()

      call.hangUp(options: nil) { (error) in
        if let e = error {
          self.message = "Leaving Teams meeting failed: " + e.localizedDescription
        } else {
          self.message = "Leaving Teams meeting was successful"
        }
      }
      self.meetingMessages.removeAll()
    } else {
      self.message = "No active call to hangup"
    }
  }

Получение данных о беседе в чате для пользователя Служб коммуникации

Сведения о собрании Teams можно получить с помощью API-интерфейсов Graph. Подробности см. в документации по Graph. SDK вызовов службы коммуникации принимает полную ссылку на встречу в Teams или идентификатор встречи. Они возвращаются как часть onlineMeeting ресурса, доступного в свойстве joinWebUrl

С помощью API Graph можно также получить threadID. Ответ имеет chatInfo объект, который содержит threadID.

Выполнение кода

Запустите приложение.

Чтобы присоединиться к собранию Teams, введите ссылку на собрание команды в пользовательском интерфейсе.

После того, как вы присоединитесь к собранию команды, необходимо разрешить доступ пользователю в клиенте Teams вашей команды. После того как пользователь будет принят и присоединяется к чату, он может отправлять и получать сообщения.

Снимок экрана: готовое приложение iOS

Примечание.

Некоторые функции в настоящее время не поддерживаются для сценариев взаимодействия с Teams. Дополнительные сведения о поддерживаемых функциях см. в разделе "Возможности собраний Teams для внешних пользователей Teams".

В этой статье описывается, как добавить чат собраний Teams в приложение с помощью пакета SDK чата Служб коммуникации Azure для Android.

Пример кода

Скачайте этот код из репозитория на GitHub в Azure Samples Подключите ваше чат-приложение к встрече в Teams.

Предварительные требования

Обеспечение взаимодействия с Teams

Пользователь Служб коммуникации, который присоединяется к собранию Teams в качестве гостевого пользователя, может получить доступ к чату собрания только после присоединения к вызову собрания Teams. Дополнительные сведения о добавлении пользователя Служб коммуникации в вызов собрания Teams см. в разделе "Взаимодействие Teams".

Чтобы использовать эту функцию, необходимо быть членом организации-владельца обоих объектов.

Присоединение к чату собрания

После включения взаимодействия Teams пользователь служб коммуникации может присоединиться к вызову Teams в качестве внешнего пользователя с помощью пакета SDK для звонков. Присоединение к вызову также добавляет их как участника в чат собрания. Из чата они могут отправлять и получать сообщения с другими пользователями по телефону. У пользователя нет доступа к сообщениям чата, отправленным до присоединения к вызову. Чтобы пользователи могли присоединиться к собраниям Teams и начать чат, выполните следующие действия.

Добавление чата в вызывающее приложение Teams

На уровне модуля build.gradle, добавьте зависимость от SDK чата.

Внимание

Известная проблема: если вы используете вместе SDK для чата и вызовов в Android в одном приложении, функция уведомлений чата в режиме реального времени не работает. Возникает проблема с разрешением зависимостей. Пока мы работаем над решением, вы можете отключить функцию уведомлений в режиме реального времени, добавив следующие исключения в зависимость от SDK для чата в файле приложения build.gradle:

implementation ("com.azure.android:azure-communication-chat:2.0.3") {
    exclude group: 'com.microsoft', module: 'trouter-client-android'
}

Добавление макета пользовательского интерфейса Teams

Замените код в activity_main.xml приведенным ниже фрагментом кода. Он добавляет входные данные для идентификатора потока и для отправки сообщений, кнопку для отправки типизированного сообщения и базовый макет чата.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/teams_meeting_thread_id"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="20dp"
        android:layout_marginTop="128dp"
        android:ems="10"
        android:hint="Meeting Thread Id"
        android:inputType="textUri"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/teams_meeting_link"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="20dp"
        android:layout_marginTop="64dp"
        android:ems="10"
        android:hint="Teams meeting link"
        android:inputType="textUri"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <LinearLayout
        android:id="@+id/button_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:gravity="center"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/teams_meeting_thread_id">

        <Button
            android:id="@+id/join_meeting_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Join Meeting" />

        <Button
            android:id="@+id/hangup_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hangup" />

    </LinearLayout>

    <TextView
        android:id="@+id/call_status_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="40dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/recording_status_bar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="20dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <ScrollView
        android:id="@+id/chat_box"
        android:layout_width="374dp"
        android:layout_height="294dp"
        android:layout_marginTop="40dp"
        android:layout_marginBottom="20dp"
        app:layout_constraintBottom_toTopOf="@+id/send_message_button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button_layout"
        android:orientation="vertical"
        android:gravity="bottom"
        android:layout_gravity="bottom"
        android:fillViewport="true">

        <LinearLayout
            android:id="@+id/chat_box_layout"
            android:orientation="vertical"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:gravity="bottom"
            android:layout_gravity="top"
            android:layout_alignParentBottom="true"/>
    </ScrollView>

    <EditText
        android:id="@+id/message_body"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginHorizontal="20dp"
        android:layout_marginTop="588dp"
        android:ems="10"
        android:inputType="textUri"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="Type your message here..."
        tools:visibility="invisible" />

    <Button
        android:id="@+id/send_message_button"
        android:layout_width="138dp"
        android:layout_height="45dp"
        android:layout_marginStart="133dp"
        android:layout_marginTop="48dp"
        android:layout_marginEnd="133dp"
        android:text="Send Message"
        android:visibility="invisible"
        app:layout_constraintBottom_toTopOf="@+id/recording_status_bar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.428"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/chat_box" />

</androidx.constraintlayout.widget.ConstraintLayout>

Включение элементов управления пользовательского интерфейса Teams

Импорт пакетов и определение переменных состояния

В содержимое MainActivity.javaдобавьте следующие импорты:

import android.graphics.Typeface;
import android.graphics.Color;
import android.text.Html;
import android.os.Handler;
import android.view.Gravity;
import android.view.View;
import android.widget.LinearLayout;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.List;
import com.azure.android.communication.chat.ChatThreadAsyncClient;
import com.azure.android.communication.chat.ChatThreadClientBuilder;
import com.azure.android.communication.chat.models.ChatMessage;
import com.azure.android.communication.chat.models.ChatMessageType;
import com.azure.android.communication.chat.models.ChatParticipant;
import com.azure.android.communication.chat.models.ListChatMessagesOptions;
import com.azure.android.communication.chat.models.SendChatMessageOptions;
import com.azure.android.communication.common.CommunicationIdentifier;
import com.azure.android.communication.common.CommunicationUserIdentifier;
import com.azure.android.core.rest.util.paging.PagedAsyncStream;
import com.azure.android.core.util.AsyncStreamHandler;

Добавьте в класс MainActivity следующие переменные:

    // InitiatorId is used to differentiate incoming messages from outgoing messages
    private static final String InitiatorId = "<USER_ID>";
    private static final String ResourceUrl = "<COMMUNICATION_SERVICES_RESOURCE_ENDPOINT>";
    private String threadId;
    private ChatThreadAsyncClient chatThreadAsyncClient;
    
    // The list of ids corresponding to messages which have already been processed
    ArrayList<String> chatMessages = new ArrayList<>();

Замените <USER_ID> идентификатором пользователя, запускающего чат. Замените <COMMUNICATION_SERVICES_RESOURCE_ENDPOINT> значением конечной точки для ресурса Служб коммуникации.

Инициализация ChatThreadClient

После присоединения к собранию создайте объект ChatThreadClient и сделайте компоненты чата видимыми.

Обновите конец MainActivity.joinTeamsMeeting() метода следующим кодом:

    private void joinTeamsMeeting() {
        ...
        EditText threadIdView = findViewById(R.id.teams_meeting_thread_id);
        threadId = threadIdView.getText().toString();
        // Initialize Chat Thread Client
        chatThreadAsyncClient = new ChatThreadClientBuilder()
                .endpoint(ResourceUrl)
                .credential(new CommunicationTokenCredential(UserToken))
                .chatThreadId(threadId)
                .buildAsyncClient();
        Button sendMessageButton = findViewById(R.id.send_message_button);
        EditText messageBody = findViewById(R.id.message_body);
        // Register the method for sending messages and toggle the visibility of chat components
        sendMessageButton.setOnClickListener(l -> sendMessage());
        sendMessageButton.setVisibility(View.VISIBLE);
        messageBody.setVisibility(View.VISIBLE);
        
        // Start the polling for chat messages immediately
        handler.post(runnable);
    }

Включение отправки сообщений

Добавьте метод sendMessage() в MainActivity. Он применяет ChatThreadClient для отправки сообщений от имени пользователя.

    private void sendMessage() {
        // Retrieve the typed message content
        EditText messageBody = findViewById(R.id.message_body);
        // Set request options and send message
        SendChatMessageOptions options = new SendChatMessageOptions();
        options.setContent(messageBody.getText().toString());
        options.setSenderDisplayName("Test User");
        chatThreadAsyncClient.sendMessage(options);
        // Clear the text box
        messageBody.setText("");
    }

Включить опрос сообщений и их отображение в приложении

Внимание

Известная проблема заключается в том, что функция уведомлений пакета SDK для чата в режиме реального времени не работает вместе с пакетами SDK для вызовов, поэтому необходимо опрашивать API GetMessages с предопределёнными интервалами. В этом примере мы используем 3-секундные интервалы.

Из списка сообщений, возвращенного API GetMessages, можно получить следующие данные:

  • сообщения text и html в потоке после присоединения;
  • изменения в списке потоков;
  • Обновления темы столбца.

MainActivity В класс добавьте обработчик и выполняемую задачу, которая выполняется через 3 секунды:

    private Handler handler = new Handler();
    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            try {
                retrieveMessages();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // Repeat every 3 seconds
            handler.postDelayed(runnable, 3000);
        }
    };

Задача уже началась в конце обновлённого метода MainActivity.joinTeamsMeeting() на этапе инициализации.

Наконец, мы добавим метод для запроса всех доступных сообщений в потоке, анализируя их по типу сообщения и отображая html их и те:text

    private void retrieveMessages() throws InterruptedException {
        // Initialize the list of messages not yet processed
        ArrayList<ChatMessage> newChatMessages = new ArrayList<>();
        
        // Retrieve all messages accessible to the user
        PagedAsyncStream<ChatMessage> messagePagedAsyncStream
                = this.chatThreadAsyncClient.listMessages(new ListChatMessagesOptions(), null);
        // Set up a lock to wait until all returned messages have been inspected
        CountDownLatch latch = new CountDownLatch(1);
        // Traverse the returned messages
        messagePagedAsyncStream.forEach(new AsyncStreamHandler<ChatMessage>() {
            @Override
            public void onNext(ChatMessage message) {
                // Messages that should be displayed in the chat
                if ((message.getType().equals(ChatMessageType.TEXT)
                    || message.getType().equals(ChatMessageType.HTML))
                    && !chatMessages.contains(message.getId())) {
                    newChatMessages.add(message);
                    chatMessages.add(message.getId());
                }
                if (message.getType().equals(ChatMessageType.PARTICIPANT_ADDED)) {
                    // Handle participants added to chat operation
                    List<ChatParticipant> participantsAdded = message.getContent().getParticipants();
                    CommunicationIdentifier participantsAddedBy = message.getContent().getInitiatorCommunicationIdentifier();
                }
                if (message.getType().equals(ChatMessageType.PARTICIPANT_REMOVED)) {
                    // Handle participants removed from chat operation
                    List<ChatParticipant> participantsRemoved = message.getContent().getParticipants();
                    CommunicationIdentifier participantsRemovedBy = message.getContent().getInitiatorCommunicationIdentifier();
                }
                if (message.getType().equals(ChatMessageType.TOPIC_UPDATED)) {
                    // Handle topic updated
                    String newTopic = message.getContent().getTopic();
                    CommunicationIdentifier topicUpdatedBy = message.getContent().getInitiatorCommunicationIdentifier();
                }
            }
            @Override
            public void onError(Throwable throwable) {
                latch.countDown();
            }
            @Override
            public void onComplete() {
                latch.countDown();
            }
        });
        // Wait until the operation completes
        latch.await(1, TimeUnit.MINUTES);
        // Returned messages should be ordered by the createdOn field to be guaranteed a proper chronological order
        // For the purpose of this demo we just reverse the list of returned messages
        Collections.reverse(newChatMessages);
        for (ChatMessage chatMessage : newChatMessages)
        {
            LinearLayout chatBoxLayout = findViewById(R.id.chat_box_layout);
            // For the purpose of this demo UI, we don't need to use HTML formatting for displaying messages
            // The Teams client always sends html messages in meeting chats 
            String message = Html.fromHtml(chatMessage.getContent().getMessage(), Html.FROM_HTML_MODE_LEGACY).toString().trim();
            TextView messageView = new TextView(this);
            messageView.setText(message);
            // Compare with sender identifier and align LEFT/RIGHT accordingly
            // Azure Communication Services users are of type CommunicationUserIdentifier
            CommunicationIdentifier senderId = chatMessage.getSenderCommunicationIdentifier();
            if (senderId instanceof CommunicationUserIdentifier
                && InitiatorId.equals(((CommunicationUserIdentifier) senderId).getId())) {
                messageView.setTextColor(Color.GREEN);
                messageView.setGravity(Gravity.RIGHT);
            } else {
                messageView.setTextColor(Color.BLUE);
                messageView.setGravity(Gravity.LEFT);
            }
            // Note: messages with the deletedOn property set to a timestamp, should be marked as deleted
            // Note: messages with the editedOn property set to a timestamp, should be marked as edited
            messageView.setTypeface(Typeface.SANS_SERIF, Typeface.BOLD);
            chatBoxLayout.addView(messageView);
        }
    }

Клиент Teams не задает отображаемые имена участников потока чата. Имена возвращаются в виде null в API для перечисления участников, в событии participantsAdded и в событии participantsRemoved. Отображаемые имена участников чата можно получить из поля remoteParticipants объекта call.

Получить чат в Teams для совещания пользователя Служб коммуникации

Сведения о собрании Teams можно получить с помощью API-интерфейсов Graph. Подробности см. в документации по Graph. Пакет SDK вызовов Служб коммуникации принимает полную ссылку на собрание Teams или идентификатор собрания. Они возвращаются как часть onlineMeeting ресурса, доступного в свойстве joinWebUrl

С помощью API Graph можно также получить threadID. Ответ имеет объект chatInfo, который содержит threadID.

Выполнение кода

Теперь вы можете запустить приложение с кнопки "Запустить приложение " на панели инструментов (SHIFT+F10).

Чтобы присоединиться к собранию и чату Teams, введите ссылку на собрание Teams и идентификатор потока в пользовательском интерфейсе.

После присоединения к собранию команды необходимо допустить пользователя в клиенте вашей команды. После того как пользователь будет принят и присоединяется к чату, он может отправлять и получать сообщения.

Снимок экрана: готовое приложение Android

Примечание.

Некоторые функции в настоящее время не поддерживаются для сценариев взаимодействия с Teams. Дополнительные сведения о поддерживаемых функциях см. в разделе "Возможности собраний Teams для внешних пользователей Teams"

В этой статье описывается, как общаться в собрании Teams с помощью пакета SDK чата Служб коммуникации Azure для C#.

Пример кода

Скачайте этот код на сайте GitHub Azure Samples Подключите ваше приложение для чата к встрече в Teams.

Предварительные условия

Присоединение к чату собрания

Пользователь Служб коммуникации может присоединиться к собранию Teams анонимно с помощью пакета SDK для вызовов. Присоединение к собранию также добавляет их в чат собрания. В чате они могут отправлять и получать сообщения с другими пользователями в собрании. Пользователь не имеет доступа к сообщениям чата, отправленным до присоединения к собранию. После окончания собрания они не могут отправлять или получать сообщения. Чтобы пользователи могли присоединиться к собраниям Teams и начать чат, выполните следующие действия.

Выполнение кода

Вы можете выполнить сборку и запустить код в Visual Studio. Поддерживаемые платформы решений: x64иx86ARM64 .

  1. Откройте экземпляр PowerShell, Терминал Windows, командную строку или эквивалентную команду и перейдите к каталогу, в который вы хотите клонировать пример.

  2. git clone https://github.com/Azure-Samples/Communication-Services-dotnet-quickstarts.git

  3. Откройте проект ChatTeamsInteropQuickStart/ChatTeamsInteropQuickStart.csproj в Visual Studio.

  4. Установите следующие или более поздние версии пакетов NuGet:

    Install-Package Azure.Communication.Calling -Version 1.0.0-beta.29
    Install-Package Azure.Communication.Chat -Version 1.1.0
    Install-Package Azure.Communication.Common -Version 1.0.1
    Install-Package Azure.Communication.Identity -Version 1.0.1
    
    
  5. Приобретя ресурс службы связи, указанный в предварительных требованиях, добавьте строку подключения в файл ChatTeamsInteropQuickStart/MainPage.xaml.cs.

    //Azure Communication Services resource connection string, i.e., = "endpoint=https://your-resource.communication.azure.net/;accesskey=your-access-key";
    private const string connectionString_ = "";
    

    Внимание

    • Выберите соответствующую платформу в раскрывающемся списке "Платформы решений" в Visual Studio перед запуском кода, например x64
    • Убедитесь, что в Windows включен режим разработчика(параметры разработчика)

    Следующие шаги не работают, если платформа настроена неправильно.

  6. Нажмите клавишу F5 , чтобы запустить проект в режиме отладки.

  7. Вставьте действительную ссылку на собрание команд в поле "Ссылка на собрания Teams " (см. следующий раздел).

  8. Конечные пользователи нажимают кнопку "Присоединиться к собранию Teams ", чтобы начать чат.

Внимание

После того как пакет SDK для вызова устанавливает подключение к собранию команд см. в разделе " Службы коммуникации" для вызова приложения Windows, ключевые функции для обработки операций чата: StartPollingForChatMessages и SendMessageButton_Click. Оба фрагмента кода находятся в ChatTeamsInteropQuickStart\MainPage.xaml.cs файле

        /// <summary>
        /// Background task that keeps polling for chat messages while the call connection is established
        /// </summary>
        private async Task StartPollingForChatMessages()
        {
            CommunicationTokenCredential communicationTokenCredential = new(user_token_);
            chatClient_ = new ChatClient(EndPointFromConnectionString(), communicationTokenCredential);
            await Task.Run(async () =>
            {
                keepPolling_ = true;

                ChatThreadClient chatThreadClient = chatClient_.GetChatThreadClient(thread_Id_);
                int previousTextMessages = 0;
                while (keepPolling_)
                {
                    try
                    {
                        CommunicationUserIdentifier currentUser = new(user_Id_);
                        AsyncPageable<ChatMessage> allMessages = chatThreadClient.GetMessagesAsync();
                        SortedDictionary<long, string> messageList = new();
                        int textMessages = 0;
                        string userPrefix;
                        await foreach (ChatMessage message in allMessages)
                        {
                            if (message.Type == ChatMessageType.Html || message.Type == ChatMessageType.Text)
                            {
                                textMessages++;
                                userPrefix = message.Sender.Equals(currentUser) ? "[you]:" : "";
                                messageList.Add(long.Parse(message.SequenceId), $"{userPrefix}{StripHtml(message.Content.Message)}");
                            }
                        }

                        //Update UI just when there are new messages
                        if (textMessages > previousTextMessages)
                        {
                            previousTextMessages = textMessages;
                            await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
                            {
                                TxtChat.Text = string.Join(Environment.NewLine, messageList.Values.ToList());
                            });

                        }
                        if (!keepPolling_)
                        {
                            return;
                        }

                        await SetInCallState(true);
                        await Task.Delay(3000);
                    }
                    catch (Exception e)
                    {
                        await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
                        {
                            _ = new MessageDialog($"An error occurred while fetching messages in PollingChatMessagesAsync(). The application will shutdown. Details : {e.Message}").ShowAsync();
                            throw e;
                        });
                        await SetInCallState(false);
                    }
                }
            });
        }
        private async void SendMessageButton_Click(object sender, RoutedEventArgs e)
        {
            SendMessageButton.IsEnabled = false;
            ChatThreadClient chatThreadClient = chatClient_.GetChatThreadClient(thread_Id_);
            _ = await chatThreadClient.SendMessageAsync(TxtMessage.Text);
            
            TxtMessage.Text = "";
            SendMessageButton.IsEnabled = true;
        }

Получите ссылку на собрание Teams с помощью Graph API, как описано в документации Graph. Ссылка возвращается как часть ресурса onlineMeeting и доступна через свойство joinWebUrl.

Вы также можете получить необходимую ссылку на собрание из URL-адреса Присоединиться к собранию в самом приглашении на собрание Teams.

Ссылка на собрание Teams выглядит следующим образом:

https://teams.microsoft.com/l/meetup-join/meeting_chat_thread_id/1606337455313?context=some_context_here`.

Если ссылка Teams имеет другой формат, необходимо получить идентификатор потока с помощью API Graph.

Снимок экрана: готовое приложение C#.

Примечание.

Некоторые функции в настоящее время не поддерживаются для сценариев взаимодействия с Teams. Дополнительные сведения о поддерживаемых функциях см. в разделе "Возможности собраний Teams для внешних пользователей Teams".

Очистка ресурсов

Если вы хотите отменить и удалить подписку на Службы коммуникации, можно удалить ресурс или группу ресурсов. При удалении группы ресурсов также удаляются все связанные с ней ресурсы. См. сведения об очистке ресурсов.