Azure Communication Services の使用を開始するには、チャット ソリューションを Microsoft Teams に接続します。
この記事では、Azure Communication Services Chat SDK for JavaScript を使用して Teams 会議でチャットする方法について説明します。
サンプル コード
GitHub Azure Samples でこのコード をダウンロードします。チャット アプリを Teams 会議に参加させます。
前提条件
- Teams のデプロイ。
- 実際に動作するチャット アプリ。
会議チャットへの参加
Communication Services ユーザーは、通話 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 会議コンテキストを入力します。 エンド ユーザーは、[ 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 として返されます。 チャット参加者の表示名は、remoteParticipants オブジェクトの call フィールドから取得できます。 名簿の変更に関する通知を受信するときに、このコードを使用して、追加または削除されたユーザーの名前を取得できます。
var displayName = call.remoteParticipants.find(p => p.identifier.communicationUserId == '<REMOTE_USER_ID>').displayName;
コードの実行
アプリをビルドして実行するには、webpack-dev-server を使用します。 ローカルの Web サーバーにアプリケーション ホストをバンドルするには、次のコマンドを実行します。
npx webpack-dev-server --entry ./client.js --output bundle.js --debug --devtool inline-source-map
ブラウザーを開き、http://localhost:8080/ に移動します。 次のスクリーンショットに示すように、アプリが起動していることがわかります。
Teams 会議のリンクをテキスト ボックスに挿入します。 ユーザーは [ Teams 会議に参加 ] をクリックして Teams 会議に参加できます。 Communication Services ユーザーが会議に参加を許可されたら、Communication Services アプリケーション内からチャットできます。 チャットを開始するには、ページの下部にあるボックスに移動します。 わかりやすくするために、このアプリケーションには、チャット内の最後の 2 つのメッセージのみが表示されます。
注意
現在、Teams との相互運用性シナリオでは、特定の機能がサポートされていません。 サポートされている機能の詳細については、「 Teams 外部ユーザー向けの Teams 会議機能」を参照してください。
この記事では、iOS 用 Azure Communication Services Chat SDK を使用して Teams 会議でチャットする方法について説明します。
サンプル コード
GitHub Azure Samples でこのコード をダウンロードします。チャット アプリを Teams 会議に参加させます。
前提条件
- アクティブなサブスクリプションが含まれる 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ファイルを開きます。
マイクへのアクセスを要求する
デバイス マイクにアクセスするには、アプリの Information Property List を NSMicrophoneUsageDescriptionで更新する必要があります。 関連する値を、ユーザーからのアクセスを要求するためにシステムによって使用されるダイアログに含まれていた string に設定します。
ターゲットの下にある [ Info ] タブを選択し、 Privacy - Microphone Usage Descriptionの文字列を追加します。
ユーザー スクリプト サンドボックスを無効にする
リンクされたライブラリ内の一部のスクリプトは、ビルド プロセス中にファイルを書き込みます。 ファイルの書き込みを有効にするには、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 を初期化します。 次に、代理人から会議の状態を確認し、会議に参加したときに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 に次のヘルパー関数を追加します。これは、可能な場合に、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 に追加します。 メッセージング イベントが発生すると、この関数が呼び出されます。
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 会議を離れると、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 を取得することもできます。 応答には、chatInfo を含む threadID オブジェクトがあります。
コードの実行
アプリケーションを実行します。
Teams 会議に参加するには、UI にチームの会議リンクを入力します。
チームの会議に参加したら、チームのクライアントでユーザーに会議への参加を許可する必要があります。 ユーザーが許可され、チャットに参加すると、メッセージを送受信できます。
注意
現在、Teams との相互運用性シナリオでは、特定の機能がサポートされていません。 サポートされている機能の詳細については、「 Teams 外部ユーザー向けの Teams 会議機能」を参照してください。
この記事では、Android 用 Azure Communication Services Chat SDK を使用して Teams 会議チャットをアプリに追加する方法について説明します。
サンプル コード
GitHub Azure Samples でこのコード をダウンロードします。チャット アプリを Teams 会議に参加させます。
前提条件
- Teams のデプロイ。
- 実際に動作する通話アプリ。
Teams の相互運用性を有効にする
ゲスト ユーザーとして Teams 会議に参加する Communication Services ユーザーは、Teams 会議通話に参加した後にのみ会議チャットにアクセスできます。 Communication Services ユーザーを Teams 会議通話に追加する方法の詳細については、「 Teams 相互運用」を参照してください。
この機能を使用するには、両方のエンティティの所有組織のメンバーである必要があります。
会議チャットへの参加
Teams の相互運用性を有効にすると、Communication Services ユーザーは Calling SDK を使用して Teams 通話に外部ユーザーとして参加できます。 通話に参加すると、会議チャットに参加者として追加されます。 チャットから、通話中に他のユーザーとメッセージを送受信できます。 ユーザーは、通話に参加する前に送信されたチャット メッセージにはアクセスできません。 エンド ユーザーが Teams 会議に参加してチャットを開始できるようにするには、次の手順を実行します。
Teams 通話アプリにチャットを追加する
モジュール レベルの build.gradle で、Chat SDK への依存関係を追加します。
重要
既知の問題: Android Chat と 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 corresponding 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 のリアルタイム通知機能は呼び出し元 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 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 として返されます。 チャット参加者の表示名は、remoteParticipants オブジェクトの call フィールドから取得できます。
Communication Services ユーザーの Teams 会議チャット スレッドを取得する
Teams 会議の詳細情報は Graph API を使用して取得できます。詳細については、Graph のドキュメントを参照してください。 Communication Services 通話 SDK は、Teams 会議の完全なリンクまたはミーティング ID を受け入れます。 これらは onlineMeeting リソースの一部として返され、joinWebUrl プロパティの下でアクセスできます
Graph API を使用して、threadID を取得することもできます。 応答には、chatInfo を含む threadID オブジェクトがあります。
コードの実行
ツール バーの [アプリの実行] ボタン (Shift + F10) から アプリ を起動できるようになりました。
Teams の会議とチャットに参加するには、Teams の会議リンクとスレッド ID を UI に入力します。
チームの会議に参加したら、チームのクライアントでユーザーに会議への参加を許可する必要があります。 ユーザーが許可され、チャットに参加すると、メッセージを送受信できます。
注意
現在、Teams との相互運用性シナリオでは、特定の機能がサポートされていません。 サポートされている機能の詳細については、「Teams 外部ユーザー向けの Teams 会議機能」を参照してください。
この記事では、C# 用 Azure Communication Services Chat SDK を使用して Teams 会議でチャットする方法について説明します。
サンプル コード
GitHub Azure Samples でこのコード をダウンロードします。チャット アプリを Teams 会議に参加させます。
前提条件
- Teams のデプロイ。
- アクティブなサブスクリプションが含まれる Azure アカウント。 無料でアカウントを作成できます。
- Visual Studio 2019 のインストール。[ユニバーサル Windows プラットフォーム開発] ワークロードを選択してください。
- デプロイ済みの Communication Services リソース。 Communication Services リソースを作成します。
- Teams 会議のリンク。
会議チャットへの参加
Communication Services ユーザーは、Calling SDK を使用して Teams 会議に匿名で参加できます。 会議に参加すると、会議チャットに参加者として追加されます。 チャットでは、会議の他のユーザーとメッセージを送受信できます。 ユーザーは、会議に参加する前に送信されたチャット メッセージにアクセスできません。 会議終了後にメッセージを送受信することはできません。 ユーザーが Teams 会議に参加してチャットを開始できるようにするには、次の手順を実行します。
コードの実行
コードは、Visual Studio でビルドして実行できます。 サポートされているソリューション プラットフォームは、 x64、x86、 ARM64です。
PowerShell、Windows ターミナル、コマンド プロンプト、またはそれと同等のインスタンスを開き、サンプルの複製先のディレクトリに移動します。
git clone https://github.com/Azure-Samples/Communication-Services-dotnet-quickstarts.gitVisual 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_ = "";重要
- コードを実行する前に、Visual Studio の [ソリューション プラットフォーム] ドロップダウン リストから適切なプラットフォームを選択します (例:
x64 - Windows で 開発者モード を有効にしていることを確認します (開発者設定)
プラットフォームが正しく構成されていない場合、次の手順は機能しません。
- コードを実行する前に、Visual Studio の [ソリューション プラットフォーム] ドロップダウン リストから適切なプラットフォームを選択します (例:
F5 キーを押して、デバッグ モードでプロジェクトを開始します。
有効なチーム会議リンクを [ Teams 会議リンク ] ボックスに貼り付けます (次のセクションを参照)。
エンド ユーザーが [ Teams 会議に参加 ] をクリックしてチャットを開始します。
重要
通話 SDK がチーム会議との接続を確立したら、 Windows アプリを呼び出す Communication Services を参照してください。チャット操作を処理する主な機能は、 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 ドキュメントで説明されているように、Graph API を使用して Teams 会議リンクを取得します。 このリンクは onlineMeeting リソースの一部として返され、joinWebUrl プロパティの下からアクセスできます。
また、必要な会議のリンクは、Teams 会議の招待状自体にある [Join Meeting](会議に参加) の URL から取得することもできます。
Teams 会議リンクは、次の URL のようになります。
https://teams.microsoft.com/l/meetup-join/meeting_chat_thread_id/1606337455313?context=some_context_here`.
Teams リンクの形式が異なる場合は、Graph API を使用してスレッド ID を取得する必要があります。
注意
現在、Teams との相互運用性シナリオでは、特定の機能がサポートされていません。 サポートされている機能の詳細については、「 Teams 外部ユーザー向けの Teams 会議機能」を参照してください。
リソースをクリーンアップする
Communication Services サブスクリプションをクリーンアップして解除する場合は、リソースまたはリソース グループを削除できます。 リソース グループを削除すると、それに関連付けられている他のリソースも削除されます。 詳細については、リソースのクリーンアップに関する記事を参照してください。
関連資料
- チャットのヒーロー サンプルを確認する
- チャットのしくみの詳細を確認する