Share via


クイックスタート: チャット アプリを Teams の会議に参加させる

Azure Communication Services の使用を開始するには、チャット ソリューションを Microsoft Teams に接続します。

このクイックスタートでは、JavaScript 用 Azure Communication Services Chat SDK を使用して Teams 会議でチャットする方法について説明します。

サンプル コード

このクイックスタートの最終的なコードは GitHub にあります。

前提条件

会議チャットへの参加

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/ に移動します。 次のスクリーンショットに示すように、アプリが起動していることがわかります。

完成した JavaScript アプリケーションのスクリーンショット。

Teams 会議のリンクをテキスト ボックスに挿入します。 Teams 会議に参加するには、 [Join Teams Meeting](Teams の会議に参加) を押します。 Communication Services ユーザーが会議への参加を許可されたら、Communication Services アプリケーション内からチャットを行うことができます。 チャットを開始するには、ページの下部にあるボックスに移動します。 わかりやすくするために、このアプリケーションには、チャット内の最後の 2 つのメッセージのみが表示されます。

注意

現在、Teams との相互運用性シナリオでは、特定の機能がサポートされていません。 サポートされている機能の詳細については、「Teams 外部ユーザーのための Teams 会議機能」を参照してください

このクイックスタートでは、iOS 用 Azure Communication Services Chat SDK を使用して Teams 会議でチャットする方法について説明します。

サンプル コード

最後までスキップしたい場合は、GitHub のサンプルとしてこのクイックスタートをダウンロードできます。

前提条件

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

詳細については、「Azure CLI を使用してアクセス トークンを作成および管理する」を参照してください。

設定

Xcode プロジェクトを作成する

Xcode で、新しい iOS プロジェクトを作成し、 [単一ビュー アプリ] テンプレートを選択します。 このチュートリアルでは SwiftUI フレームワークを使用します。そのため、[Language](言語) を [Swift] に設定し、[User Interface](ユーザー インターフェイス) を [SwiftUI] に設定する必要があります。 このクイック スタートでは、テストは作成しません。 [Include Tests](テストを含める) チェック ボックスはオフにしてかまいません。

Xcode 内で新しいプロジェクトを作成するウィンドウのスクリーンショット。

CocoaPods のインストール

このガイドを使用して、お使いの Mac に CocoaPods をインストールしてください。

CocoaPods でパッケージと依存関係をインストールする

  1. アプリケーションの Podfile を作成するために、ターミナルを開いてプロジェクト フォルダーに移動し、pod init を実行します。

  2. 次のコードをターゲットの下にある Podfile に追加して保存します。

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

  # Pods for Chat Teams Interop
  pod 'AzureCommunicationCalling'
  pod 'AzureCommunicationChat'
  
end
  1. pod install を実行します。

  2. Xcode を使用して .xcworkspace ファイルを開きます。

マイクへのアクセスを要求する

デバイスのマイクにアクセスするには、アプリの情報プロパティ リストを NSMicrophoneUsageDescription によって更新する必要があります。 関連する値を、ユーザーからのアクセスを要求するためにシステムによって使用されるダイアログに含まれていた string に設定します。

ターゲットで、Info タブを選択し、[プライバシー - マイクの使用方法の説明] に文字列を追加します

Xcode 内でのマイク使用の追加を示すスクリーンショット。

ユーザー スクリプト サンドボックスを無効にする

リンクされたライブラリ内の一部のスクリプトは、ビルド プロセス中にファイルを書き込みます。 これを許可するには、Xcode でユーザー スクリプト サンドボックスを無効にします。 ビルド設定で、sandbox を検索し、User Script SandboxingNo に設定します。

Xcode 内でのユーザー スクリプト サンドボックスの無効化を示すスクリーンショット。

会議チャットへの参加

Communication Services ユーザーは、Calling SDK を使用して、匿名ユーザーとして Teams 会議に参加できます。 ユーザーが Teams 会議に参加すると、他の会議出席者とメッセージを送受信できるようになります。 ユーザーは、参加前に送信されたチャット メッセージにアクセスすることや、会議に参加していない状態でメッセージを送受信することはできません。 会議に参加してチャットを開始するには、次の手順に従います。

アプリのフレームワークを設定する

次のスニペットを追加して、ContentView.swift に Azure Communication パッケージをインポートします。

import AVFoundation
import SwiftUI

import AzureCommunicationCalling
import AzureCommunicationChat

ContentView.swiftstruct 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 関数に次のコードを追加して、CallClientChatClient を初期化します。

  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 を初期化します。 これにより、代表者から会議の状態を確認し、会議に参加するときに threadIdChatThreadClient を初期化する必要があります。

次のコードを使用して 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 にチームの会議リンクを入力します。

チームの会議に参加したら、チームのクライアントでユーザーに会議への参加を許可する必要があります。 ユーザーが許可され、チャットに参加すると、メッセージを送受信できるようになります。

完了した iOS アプリケーションのスクリーンショット。

注意

現在、Teams との相互運用性シナリオでは、特定の機能がサポートされていません。 サポートされている機能の詳細については、「Teams 外部ユーザーのための Teams 会議機能」を参照してください

このクイックスタートでは、Android 用 Azure Communication Services Chat SDK を使用して Teams 会議でチャットする方法について説明します。

サンプル コード

最後までスキップしたい場合は、GitHub のサンプルとしてこのクイックスタートをダウンロードできます。

前提条件

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 から返されるメッセージ一覧から次のデータを取得できます。

  • 参加後のスレッド上の texthtml のメッセージ
  • スレッド参加者一覧の変更
  • スレッド トピックの更新

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() メソッドの最後で、タスクが既に開始されていることに注意してください。

最後に、スレッド上のアクセス可能なすべてのメッセージのクエリを実行し、メッセージの種類ごとに解析し、htmltext のものを表示するメソッドを追加します。

    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 に入力します。

チームの会議に参加したら、チームのクライアントでユーザーに会議への参加を許可する必要があります。 ユーザーが許可され、チャットに参加すると、メッセージを送受信できるようになります。

完成した Android アプリケーションのスクリーンショット。

注意

現在、Teams との相互運用性シナリオでは、特定の機能がサポートされていません。 サポートされている機能の詳細については、「Teams 外部ユーザーのための Teams 会議機能」を参照してください

このクイックスタートでは、C# の Azure Communication Services Chat SDK を使用して Teams 会議でチャットする方法について説明します。

サンプル コード

このクイックスタートのコードは GitHub にあります。

前提条件

会議チャットへの参加

Communication Services ユーザーは、Calling SDK を使用して、匿名ユーザーとして Teams 会議に参加できます。 会議に参加すると、会議チャットにも参加者として追加されます。会議チャットでは、会議に参加している他のユーザーとメッセージを送受信できます。 ユーザーは、会議に参加する前に送信されていたチャット メッセージにはアクセスできず、会議の終了後にメッセージを送受信することもできません。 会議に参加してチャットを開始するには、次の手順に従います。

コードの実行

コードは、Visual Studio でビルドして実行できます。 Microsoft がサポートしているソリューション プラットフォーム (x64x86ARM64) に注意してください。

  1. PowerShell、Windows ターミナル、コマンド プロンプト、またはそれと同等のインスタンスを開き、サンプルの複製先のディレクトリに移動します。
  2. git clone https://github.com/Azure-Samples/Communication-Services-dotnet-quickstarts.git
  3. Visual Studio でプロジェクト ChatTeamsInteropQuickStart/ChatTeamsInteropQuickStart.csproj を開きます。
  4. 次の NuGet パッケージ バージョン (またはそれ以降) をインストールします。
Install-Package Azure.Communication.Calling -Version 1.0.0-beta.29
Install-Package Azure.Communication.Chat -Version 1.1.0
Install-Package Azure.Communication.Common -Version 1.0.1
Install-Package Azure.Communication.Identity -Version 1.0.1

  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 で '開発者モード' が有効になっていることを確認します (開発者の設定)

適切に構成されていない場合、次の手順はうまくいきません

  1. F5 キーを押して、プロジェクトをデバッグ モードで起動します。
  2. [Teams 会議のリンク] ボックスに有効な Teams 会議のリンクを貼り付けます (次のセクションを参照)
  3. [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 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 会議のリンクは 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 を取得する必要があります。

完了した C# アプリケーションのスクリーンショット。

注意

現在、Teams との相互運用性シナリオでは、特定の機能がサポートされていません。 サポートされている機能の詳細については、「Teams 外部ユーザーのための Teams 会議機能」を参照してください

リソースをクリーンアップする

Communication Services サブスクリプションをクリーンアップして解除する場合は、リソースまたはリソース グループを削除できます。 リソース グループを削除すると、それに関連付けられている他のリソースも削除されます。 詳細については、リソースのクリーンアップに関する記事を参照してください。

次の手順

詳細については、次の記事を参照してください。