クイックスタート: チャット アプリを Teams の会議に参加させる
Azure Communication Services の使用を開始するには、チャット ソリューションを Microsoft Teams に接続します。
このクイックスタートでは、JavaScript 用 Azure Communication Services Chat SDK を使用して Teams 会議でチャットする方法について説明します。
サンプル コード
このクイックスタートの最終的なコードは GitHub にあります。
前提条件
- Teams のデプロイ。
- 実際に動作するチャット アプリ。
会議チャットへの参加
Communication Services ユーザーは、Calling SDK を使用して、匿名ユーザーとして Teams 会議に参加できます。 会議に参加すると、会議チャットにも参加者として追加されます。会議チャットでは、会議に参加している他のユーザーとメッセージを送受信できます。 ユーザーは、会議に参加する前に送信されたチャット メッセージにアクセスすることはできず、会議の終了後はメッセージを送受信することはできなくなります。 会議に参加してチャットを開始するには、次の手順に従います。
新しい Node.js アプリケーションを作成する
ターミナルまたはコマンド ウィンドウを開き、アプリ用の新しいディレクトリを作成して、そこに移動します。
mkdir chat-interop-quickstart && cd chat-interop-quickstart
既定の設定で npm init -y
を実行して、package.json ファイルを作成します。
npm init -y
チャット パッケージをインストールする
npm install
コマンドを使用して、JavaScript 用の必要な Communication Services SDK をインストールします。
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 を使用してアプリケーション資産をバンドルします。 次のコマンドを実行して、Webpack、webpack-cli、および webpack-dev-server npm パッケージをインストールし、package.json 内の開発依存関係として表示します。
npm install webpack@5.89.0 webpack-cli@5.1.4 webpack-dev-server@4.15.1 --save-dev
自分のプロジェクトのルート ディレクトリに、index.html ファイルを作成します。 このファイルを使用して、ユーザーが会議に参加してチャットを開始できるようにする基本的なレイアウトを構成します。
Teams の UI コントロールを追加する
index.html のコードを次のスニペットに置き換えます。 ページの上部にあるテキスト ボックスは、Teams 会議のコンテキストを入力するために使用されます。 [Join Teams Meeting](Teams の会議に参加) ボタンは、指定した会議に参加するために使用します。 ページの下部にチャットのポップアップが表示されます。 これは、会議スレッドでメッセージを送信するために使用でき、Communication Services ユーザーがメンバーである間にそのスレッドで送信されたメッセージがリアルタイムで表示されます。
<!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 の UI コントロールを有効にする
client.js ファイルの内容を次のスニペットに置き換えます。
スニペット内で、以下を置き換えます
SECRET_CONNECTION_STRING
を Communication Services の接続文字列に
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 クライアントによって設定されません。 これらの名前は、participantsAdded
イベントと participantsRemoved
イベントの、参加者を一覧表示する API では null として返されます。 チャット参加者の表示名は、call
オブジェクトの remoteParticipants
フィールドから取得できます。 名簿の変更に関する通知を受信するときに、このコードを使用して、追加または削除されたユーザーの名前を取得できます。
var displayName = call.remoteParticipants.find(p => p.identifier.communicationUserId == '<REMOTE_USER_ID>').displayName;
コードの実行
Webpack ユーザーは、webpack-dev-server
を使用してアプリをビルドし、実行することができます。 ローカルの Web サーバーにアプリケーション ホストをバンドルするには、次のコマンドを実行します。
npx webpack-dev-server --entry ./client.js --output bundle.js --debug --devtool inline-source-map
ブラウザーを開き、http://localhost:8080/
に移動します。 次のスクリーンショットに示すように、アプリが起動していることがわかります。
Teams 会議のリンクをテキスト ボックスに挿入します。 Teams 会議に参加するには、 [Join Teams Meeting](Teams の会議に参加) を押します。 Communication Services ユーザーが会議への参加を許可されたら、Communication Services アプリケーション内からチャットを行うことができます。 チャットを開始するには、ページの下部にあるボックスに移動します。 わかりやすくするために、このアプリケーションには、チャット内の最後の 2 つのメッセージのみが表示されます。
注意
現在、Teams との相互運用性シナリオでは、特定の機能がサポートされていません。 サポートされている機能の詳細については、「Teams 外部ユーザーのための Teams 会議機能」を参照してください
このクイックスタートでは、iOS 用 Azure Communication Services Chat SDK を使用して Teams 会議でチャットする方法について説明します。
サンプル コード
最後までスキップしたい場合は、GitHub のサンプルとしてこのクイックスタートをダウンロードできます。
前提条件
- アクティブなサブスクリプションが含まれる Azure アカウント。 無料でアカウントを作成する
- Xcode を実行しており、有効な開発者証明書がキーチェーンにインストールされている Mac。
- Teams のデプロイ。
- Azure Communication Service のユーザー アクセス トークン。 Azure CLI を使用し、接続文字列を指定してコマンドを実行して、ユーザーとアクセス トークンを作成することもできます。
az communication user-identity token issue --scope voip chat --connection-string "yourConnectionString"
詳細については、「Azure CLI を使用してアクセス トークンを作成および管理する」を参照してください。
設定
Xcode プロジェクトを作成する
Xcode で、新しい iOS プロジェクトを作成し、 [単一ビュー アプリ] テンプレートを選択します。 このチュートリアルでは SwiftUI フレームワークを使用します。そのため、[Language](言語) を [Swift] に設定し、[User Interface](ユーザー インターフェイス) を [SwiftUI] に設定する必要があります。 このクイック スタートでは、テストは作成しません。 [Include Tests](テストを含める) チェック ボックスはオフにしてかまいません。
CocoaPods のインストール
このガイドを使用して、お使いの Mac に CocoaPods をインストールしてください。
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
を実行します。Xcode を使用して
.xcworkspace
ファイルを開きます。
マイクへのアクセスを要求する
デバイスのマイクにアクセスするには、アプリの情報プロパティ リストを NSMicrophoneUsageDescription
によって更新する必要があります。 関連する値を、ユーザーからのアクセスを要求するためにシステムによって使用されるダイアログに含まれていた string
に設定します。
ターゲットで、Info
タブを選択し、[プライバシー - マイクの使用方法の説明] に文字列を追加します
ユーザー スクリプト サンドボックスを無効にする
リンクされたライブラリ内の一部のスクリプトは、ビルド プロセス中にファイルを書き込みます。 これを許可するには、Xcode でユーザー スクリプト サンドボックスを無効にします。
ビルド設定で、sandbox
を検索し、User Script Sandboxing
を No
に設定します。
会議チャットへの参加
Communication Services ユーザーは、Calling SDK を使用して、匿名ユーザーとして Teams 会議に参加できます。 ユーザーが Teams 会議に参加すると、他の会議出席者とメッセージを送受信できるようになります。 ユーザーは、参加前に送信されたチャット メッセージにアクセスすることや、会議に参加していない状態でメッセージを送受信することはできません。 会議に参加してチャットを開始するには、次の手順に従います。
アプリのフレームワークを設定する
次のスニペットを追加して、ContentView.swift
に Azure Communication パッケージをインポートします。
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>
を、実際の Communication Services リソースのエンドポイントに置き換えます。
Azure クライアントのコマンド ラインを使用して、<ADD_YOUR_USER_TOKEN_HERE>
を上記で生成されたトークンに置き換えます。
ユーザー アクセス トークンの詳細については、ユーザー アクセス トークンに関する記事をお読みください。
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] = []
次に、UI 要素を保持する本文変数を追加します。 このクイック スタートでは、これらのコントロールにビジネス ロジックをアタッチします。 次のコードを 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
を初期化します。 これにより、代表者から会議の状態を確認し、会議に参加するときに threadId
で ChatThreadClient
を初期化する必要があります。
次のコードを使用して 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
に次のヘルパー関数を追加します。これは、可能な場合に、Team の会議リンクからチャット スレッド ID を解析します。 この抽出に失敗した場合、ユーザーは Graph API を使用してチャット スレッド ID を手動で入力してスレッド ID を取得する必要があります。
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
イベントのハンドラーを実装します。 新しいメッセージがスレッドに送信されると、それらを UI に表示できるように、このハンドラーによって meetingMessages
変数にメッセージが追加されます。
まず、次の構造体を ContentView.swift
に追加します。 UI では、構造体内のデータを使用してチャット メッセージを表示します。
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
に追加します。 これは、メッセージング イベントが発生したときに呼び出されます。 chatClient?.register()
メソッドを使用して、switch
ステートメントで処理するすべてのイベントに登録する必要があることにご注意ください。
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"
}
}
}
チャットから退出する
ユーザーが Team の会議から退出すると、UI からチャット メッセージがクリアされ、通話が切れます。 完全なコードを以下に示します。
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"
}
}
Communication Services ユーザーの Teams 会議チャット スレッドを取得する
Teams 会議の詳細情報は Graph API を使用して取得できます。詳細については、Graph のドキュメントを参照してください。 Communication Services 通話 SDK は、Teams 会議の完全なリンクまたはミーティング ID を受け入れます。 これらは onlineMeeting
リソースの一部として返され、joinWebUrl
プロパティの下でアクセスできます
Graph API を使用して、threadID
を取得することもできます。 応答には、threadID
を含む chatInfo
オブジェクトがあります。
コードの実行
アプリケーションを実行します。
Teams 会議に参加するには、UI にチームの会議リンクを入力します。
チームの会議に参加したら、チームのクライアントでユーザーに会議への参加を許可する必要があります。 ユーザーが許可され、チャットに参加すると、メッセージを送受信できるようになります。
注意
現在、Teams との相互運用性シナリオでは、特定の機能がサポートされていません。 サポートされている機能の詳細については、「Teams 外部ユーザーのための Teams 会議機能」を参照してください
このクイックスタートでは、Android 用 Azure Communication Services Chat SDK を使用して Teams 会議でチャットする方法について説明します。
サンプル コード
最後までスキップしたい場合は、GitHub のサンプルとしてこのクイックスタートをダウンロードできます。
前提条件
- Teams のデプロイ。
- 実際に動作する通話アプリ。
Teams の相互運用性を有効にする
ゲスト ユーザーとして Teams に参加している Communication Services ユーザーは、Teams の会議通話に参加したときにのみ会議のチャットにアクセスできます。 Communication Services ユーザーを Teams の会議通話に追加する方法については、Teams の相互運用性に関するドキュメントを参照してください。
この機能を使用するには、両方のエンティティの所有組織のメンバーである必要があります。
会議チャットへの参加
Teams の相互運用性が有効になると、Communication Services ユーザーは、Calling SDK を使用して外部ユーザーとして Teams の通話に参加できます。 通話に参加すると、会議チャットにも参加者として追加されます。会議チャットでは、通話の他のユーザーとメッセージを送受信できます。 ユーザーは、通話に参加する前に送信されたチャット メッセージにはアクセスできません。 会議に参加してチャットを開始するには、次の手順に従います。
Teams 通話アプリにチャットを追加する
モジュール レベルの build.gradle
で、Chat SDK への依存関係を追加します。
重要
既知の問題: 同じアプリケーションで Android チャットと Calling SDK を一緒に使用している場合、Chat SDK のリアルタイム通知機能は機能しません。 依存関係の解決の問題が発生します。 Microsoft が解決策に取り組んでいる間は、アプリの build.gradle
ファイル内の Chat SDK の依存関係に次の除外を追加することで、リアルタイム通知機能をオフにすることができます。
implementation ("com.azure.android:azure-communication-chat:2.0.3") {
exclude group: 'com.microsoft', module: 'trouter-client-android'
}
Teams UI レイアウトを追加する
activity_main.xml のコードを次のスニペットで置き換えます。 これにより、スレッド ID とメッセージ送信の入力、入力したメッセージを送信するボタン、基本的なチャット レイアウトが追加されます。
<?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 の UI コントロールを有効にする
パッケージのインポートと状態変数の定義
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>
を、チャットを開始したユーザーの ID に置き換えます。
<COMMUNICATION_SERVICES_RESOURCE_ENDPOINT>
を、実際の Communication Services リソースのエンドポイントに置き換えます。
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("");
}
メッセージのポーリングとアプリケーションでのレンダリングを有効にする
重要
既知の問題: Chat SDK のリアルタイム通知機能は Calling SDK の機能と連携していないため、事前に定義された間隔で GetMessages
API をポーリングする必要があります。 このサンプルでは、3 秒間隔を使用します。
GetMessages
API から返されるメッセージ一覧から次のデータを取得できます。
- 参加後のスレッド上の
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 クライアントによって設定されません。 これらの名前は、participantsAdded
イベントと participantsRemoved
イベントの、参加者を一覧表示する API では null として返されます。 チャット参加者の表示名は、call
オブジェクトの remoteParticipants
フィールドから取得できます。
Communication Services ユーザーの Teams 会議チャット スレッドを取得する
Teams 会議の詳細情報は Graph API を使用して取得できます。詳細については、Graph のドキュメントを参照してください。 Communication Services 通話 SDK は、Teams 会議の完全なリンクまたはミーティング ID を受け入れます。 これらは onlineMeeting
リソースの一部として返され、joinWebUrl
プロパティの下でアクセスできます
Graph API を使用して、threadID
を取得することもできます。 応答には、threadID
を含む chatInfo
オブジェクトがあります。
コードの実行
ツール バーの [Run App](アプリの実行) ボタン (Shift + F10) を使用してアプリを起動できるようになりました。
Teams の会議とチャットに参加するには、Teams の会議リンクとスレッド ID を UI に入力します。
チームの会議に参加したら、チームのクライアントでユーザーに会議への参加を許可する必要があります。 ユーザーが許可され、チャットに参加すると、メッセージを送受信できるようになります。
注意
現在、Teams との相互運用性シナリオでは、特定の機能がサポートされていません。 サポートされている機能の詳細については、「Teams 外部ユーザーのための Teams 会議機能」を参照してください
このクイックスタートでは、C# の Azure Communication Services Chat SDK を使用して Teams 会議でチャットする方法について説明します。
サンプル コード
このクイックスタートのコードは GitHub にあります。
前提条件
- Teams のデプロイ。
- アクティブなサブスクリプションが含まれる Azure アカウント。 無料でアカウントを作成できます。
- Visual Studio 2019 のインストール。[ユニバーサル Windows プラットフォーム開発] ワークロードを選択してください。
- デプロイ済みの Communication Services リソース。 Communication Services リソースを作成します。
- Teams 会議のリンク。
会議チャットへの参加
Communication Services ユーザーは、Calling SDK を使用して、匿名ユーザーとして Teams 会議に参加できます。 会議に参加すると、会議チャットにも参加者として追加されます。会議チャットでは、会議に参加している他のユーザーとメッセージを送受信できます。 ユーザーは、会議に参加する前に送信されていたチャット メッセージにはアクセスできず、会議の終了後にメッセージを送受信することもできません。 会議に参加してチャットを開始するには、次の手順に従います。
コードの実行
コードは、Visual Studio でビルドして実行できます。 Microsoft がサポートしているソリューション プラットフォーム (x64
、x86
、ARM64
) に注意してください。
- PowerShell、Windows ターミナル、コマンド プロンプト、またはそれと同等のインスタンスを開き、サンプルの複製先のディレクトリに移動します。
git clone https://github.com/Azure-Samples/Communication-Services-dotnet-quickstarts.git
- Visual Studio でプロジェクト ChatTeamsInteropQuickStart/ChatTeamsInteropQuickStart.csproj を開きます。
- 次の 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
- 前提条件で入手済みの Communication Services リソースを使用して、ChatTeamsInteropQuickStart/MainPage.xaml.cs ファイルに connectionstring を追加します。
//Azure Communication Services resource connection string, i.e., = "endpoint=https://your-resource.communication.azure.net/;accesskey=your-access-key";
private const string connectionString_ = "";
重要
- コード (
x64
) を実行する前に、Visual Studio の [Solution Platforms] から適切なプラットフォームを選択します。 - Windows 10 で '開発者モード' が有効になっていることを確認します (開発者の設定)
適切に構成されていない場合、次の手順はうまくいきません
- F5 キーを押して、プロジェクトをデバッグ モードで起動します。
- [Teams 会議のリンク] ボックスに有効な Teams 会議のリンクを貼り付けます (次のセクションを参照)
- [Teams 会議に参加] を押して、チャットを開始します。
重要
呼び出し元の SDK によって Teams 会議との接続が確立されたら (Communication Services 通話 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 会議のリンクを取得する
Teams 会議のリンクは Graph API を使用して取得できます。詳細については、Graph のドキュメントを参照してください。 このリンクは onlineMeeting
リソースの一部として返され、joinWebUrl
プロパティの下からアクセスできます。
また、必要な会議のリンクは、Teams 会議の招待状自体にある [Join Meeting](会議に参加) の URL から取得することもできます。
Teams の会議リンクは、https://teams.microsoft.com/l/meetup-join/meeting_chat_thread_id/1606337455313?context=some_context_here
のようになっています。
チームのリンクの形式がこれと異なる場合は、Graph API を使用してスレッド ID を取得する必要があります。
注意
現在、Teams との相互運用性シナリオでは、特定の機能がサポートされていません。 サポートされている機能の詳細については、「Teams 外部ユーザーのための Teams 会議機能」を参照してください
リソースをクリーンアップする
Communication Services サブスクリプションをクリーンアップして解除する場合は、リソースまたはリソース グループを削除できます。 リソース グループを削除すると、それに関連付けられている他のリソースも削除されます。 詳細については、リソースのクリーンアップに関する記事を参照してください。
次の手順
詳細については、次の記事を参照してください。
- チャットのヒーロー サンプルを確認する
- チャットのしくみの詳細を確認する