Краткое руководство. Присоединение приложения чата к собранию Teams
Приступите к работе со Службами коммуникации Azure, подключив решение для чата к Microsoft Teams.
В этом кратком руководстве вы узнаете, как общаться в чате на собрании Teams с помощью пакета SDK для чата Служб коммуникации Azure для JavaScript.
Пример кода
Итоговый код для этого краткого руководства можно найти на сайте GitHub.
Необходимые компоненты
- Развертывание 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.
Настройка платформы приложения
В этом кратком руководстве используется Webpack для упаковки ресурсов приложения. Выполните следующую команду, чтобы установить пакеты npm webpack, webpack-cli и webpack-dev-server и перечислить их в качестве зависимостей разработки в package.json:
npm install webpack@5.89.0 webpack-cli@5.1.4 webpack-dev-server@4.15.1 --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 могут использовать webpack-dev-server
для сборки и запуска приложения. Выполните следующую команду, чтобы создать пакет узла приложения на локальном веб-сервере:
npx webpack-dev-server --entry ./client.js --output bundle.js --debug --devtool inline-source-map
Откройте браузер и перейдите по адресу http://localhost:8080/
. Приложение должно быть запущено, как показано на следующем снимке экрана:
Вставьте ссылку на собрание Teams в текстовое поле. Чтобы присоединиться к собранию в Teams, нажмите кнопку Join Teams Meeting (Присоединиться к собранию в Teams). После того, как пользователь Службы коммуникации был допущен к встрече, вы можете общаться в чате из приложения служб связи. Чтобы начать беседу, перейдите к полю в нижней части страницы. Для простоты приложение отображает только последние два сообщения в чате.
Примечание.
Некоторые функции в настоящее время не поддерживаются для сценариев взаимодействия с Teams. Дополнительные сведения о поддерживаемых функциях см. в разделе "Возможности собраний Teams для внешних пользователей Teams"
В этом кратком руководстве описывается, как общаться в чате конференции Teams с помощью пакета SDK для чата Служб коммуникации Azure для iOS.
Пример кода
Если вы хотите сразу перейти к завершающему этапу, можно скачать это краткое руководство в качестве примера с портала GitHub.
Необходимые компоненты
- Учетная запись Azure с активной подпиской. Создание бесплатной учетной записи
- компьютер Mac с Xcode, а также действительный сертификат разработчика, установленный в цепочку ключей;
- Развертывание Teams.
- Маркер доступа пользователя для Службы коммуникации Azure. Вы также можете использовать Azure CLI и выполнить команду с строка подключения для создания пользователя и маркера доступа.
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 (Включить тесты).
Установка CocoaPods
Используйте это руководство для установки CocoaPods на компьютере Mac.
Установка пакета и его зависимостей с помощью CocoaPods
Чтобы создать
Podfile
приложение, откройте терминал и перейдите в папку проекта и запустите pod init.Добавьте следующий код в целевой
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
Запустите
pod install
..xcworkspace
Откройте файл с помощью Xcode.
Запрос доступа к микрофону
Чтобы получить доступ к микрофону устройства, вам необходимо указать ключ NSMicrophoneUsageDescription
в списке свойств сведений приложения. Для параметра string
, связанного с ним, включенного в диалоговое окно, система использует для запроса доступа от пользователя.
В целевом объекте выберите вкладку Info
и добавьте строку для параметра "Конфиденциальность — описание использования микрофона"
Отключение песочницы сценариев пользователя
Некоторые скрипты в связанных библиотеках записывают файлы во время сборки. Чтобы разрешить это, отключите песочницу пользовательского скрипта в Xcode.
В параметрах сборки найдите sandbox
и установите значение User Script Sandboxing
No
.
Присоединение к чату собрания
Пользователь Служб коммуникации может анонимно присоединиться к собранию 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] = []
Теперь добавим основной var основного текста для хранения элементов пользовательского интерфейса. Мы присоединяем бизнес-логику к этим элементам управления в этом кратком руководстве. Добавьте следующий код в структуру 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
, используемую для синтаксического анализа идентификатора потока чата из ссылки на собрание команды, если это возможно. В случае сбоя извлечения пользователю потребуется вручную ввести идентификатор потока чата с помощью 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"
}
}
}
Выход из чата
Когда пользователь покидает собрание команды, мы очищаем сообщения чата из пользовательского интерфейса и повесим звонок. Полный код показан ниже.
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. Дополнительные сведения о поддерживаемых функциях см. в разделе "Возможности собраний Teams для внешних пользователей Teams"
В этом кратком руководстве описывается, как общаться в чате собрании Teams с помощью пакета SDK для чата Служб коммуникации Azure для Android.
Пример кода
Если вы хотите сразу перейти к завершающему этапу, можно скачать это краткое руководство в качестве примера с портала GitHub.
Необходимые компоненты
- Развертывание Teams.
- Рабочее вызывающее приложение.
Обеспечение взаимодействия с Teams
Пользователь Служб коммуникации, который присоединяется к собранию в Teams в качестве гостевого пользователя, может получить доступ к чату собрания, только когда он присоединится к вызову собрания в Teams. Дополнительные сведения о добавлении пользователя Служб коммуникации в вызов собрания в Teams см. в документации по взаимодействию с Teams.
Для использования этой функции пользователь должен быть членом организации-владельца обеих сущностей.
Присоединение к чату собрания
Включив взаимодействие с Teams, пользователь Служб коммуникации может присоединиться в качестве внешнего пользователя к вызову в Teams с помощью пакета SDK для вызовов. Присоединение к вызову добавляет их в чат собрания, а также, где они могут отправлять и получать сообщения с другими пользователями по вызову. У пользователя нет доступа к сообщениям чата, отправленным до присоединения к вызову. Чтобы присоединиться к собранию и начать беседу, можно выполнить следующие действия.
Добавление чата в вызывающее приложение Teams
На уровне build.gradle
модуля добавьте зависимость от пакета SDK чата.
Внимание
Известная проблема: при использовании в Android пакетов SDK для вызовов и чатов вместе в одном приложении функция уведомлений в режиме реального времени пакета SDK для чата не работает. Возникает проблема с разрешением зависимости. Пока мы работаем над решением, вы можете отключить функцию уведомлений в режиме реального времени, добавив следующие исключения в зависимость от 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 corresponsding 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 will 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 можно получить с помощью API-интерфейсов Graph. Подробности см. в документации по Graph. Пакет SDK вызовов Служб коммуникации принимает полную ссылку на собрание Teams или идентификатор собрания. Они возвращаются как часть onlineMeeting
ресурса, доступного в свойстве joinWebUrl
С помощью API Graph можно также получить threadID
. Ответ содержит chatInfo
объект, содержащий threadID
объект .
Выполнение кода
Теперь вы можете запустить приложение с помощью кнопки Run App (Запустить приложение) на панели инструментов или нажав клавиши SHIFT+F10.
Чтобы присоединиться к собранию и чату Teams, введите ссылку на собрание Teams и идентификатор потока в пользовательском интерфейсе.
После присоединения к собранию команды необходимо признать пользователя на собрание в клиенте вашей команды. После того как пользователь признался и присоединился к чату, вы сможете отправлять и получать сообщения.
Примечание.
Некоторые функции в настоящее время не поддерживаются для сценариев взаимодействия с Teams. Дополнительные сведения о поддерживаемых функциях см. в разделе "Возможности собраний Teams для внешних пользователей Teams"
Из этого краткого руководства вы узнаете, как общаться в собрании Teams с помощью пакета SDK для чата Службы коммуникации Azure для C#.
Пример кода
Код для этого краткого руководства можно найти на сайте GitHub.
Необходимые компоненты
- Развертывание Teams.
- Учетная запись Azure с активной подпиской. Создайте учетную запись бесплатно .
- Установите Visual Studio 2019 с рабочей нагрузкой разработки для универсальной платформы Windows.
- Развернутый ресурс Служб коммуникации. Создайте ресурс Служб коммуникации.
- Ссылка на собрание Teams.
Присоединение к чату собрания
Пользователь Служб коммуникации может анонимно присоединиться к собранию Teams с помощью пакета SDK для вызовов. Присоединение к собранию также добавляет их в чат собрания, где они могут отправлять и получать сообщения с другими пользователями в собрании. Пользователь не получит доступ к сообщениям чата, отправленным до присоединения к собранию, и после завершения собрания они не смогут отправлять или получать сообщения. Чтобы присоединиться к собранию и начать беседу, можно выполнить следующие действия.
Выполнение кода
Вы можете выполнить сборку и запустить код в Visual Studio. Обратите внимание на поддерживаемые платформы решений: x64
,x86
и ARM64
.
- Откройте экземпляр PowerShell, Терминал Windows, командную строку или эквивалентную команду и перейдите к каталогу, в который вы хотите клонировать пример.
git clone https://github.com/Azure-Samples/Communication-Services-dotnet-quickstarts.git
- Откройте проект ChatTeamsInteropQuickStart/ChatTeamsInteropQuickStart.csproj в Visual Studio.
- Установите следующие или более поздние версии пакетов 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
- При использовании ресурса служб коммуникации, приобретенного в предварительных требованиях, добавьте строку подключения в файл 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 10 включен режим разработчика (Параметры разработчика).
Если эти настройки заданы неправильно, следующие шаги не удастся выполнить.
- Нажмите клавишу F5, чтобы запустить проект в режиме отладки.
- Вставьте допустимую ссылку на собрание Teams в соответствующем поле (см. следующий раздел).
- Нажмите кнопку "Присоединиться к собранию в Teams", чтобы начать беседу.
Внимание
Когда пакет SDK для звонков установит подключение к собранию Teams (см. раздел о приложении Windows для вызовов с помощью Служб коммуникации), ключевыми функциями для управления операциями чата будут StartPollingForChatMessages и SendMessageButton_Click. Оба фрагмента кода находятся в ChatTeamsInteropQuickStart\MainPage.xaml.cs.
/// <summary>
/// Background task that keeps polling for chat messages while the call connection is stablished
/// </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
Ссылку на собрание Teams можно получить с помощью интерфейсов API Graph. Подробности см. в документации по Graph. Эта ссылка возвращается как часть ресурса onlineMeeting
, доступного под свойством joinWebUrl
.
Вы также можете получить необходимую ссылку на собрание из URL-адреса Присоединиться к собранию в самом приглашении на собрание Teams.
Ссылка на собрание в Teams выглядит следующим образом: https://teams.microsoft.com/l/meetup-join/meeting_chat_thread_id/1606337455313?context=some_context_here
.
Если у вашей команды есть другой формат, необходимо получить идентификатор потока с помощью API Graph.
Примечание.
Некоторые функции в настоящее время не поддерживаются для сценариев взаимодействия с Teams. Дополнительные сведения о поддерживаемых функциях см. в разделе "Возможности собраний Teams для внешних пользователей Teams"
Очистка ресурсов
Если вы хотите отменить и удалить подписку на Службы коммуникации, можно удалить ресурс или группу ресурсов. При удалении группы ресурсов также удаляются все связанные с ней ресурсы. См. сведения об очистке ресурсов.
Следующие шаги
Дополнительные сведения см. в следующих статьях:
- Ознакомьтесь с нашим главным примером функции чата.
- См. дополнительные сведения о принципах работы чата.