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


Краткое руководство. Присоединение приложения чата к собранию Teams

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

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

Пример кода

Итоговый код для этого краткого руководства можно найти на сайте GitHub.

Необходимые компоненты

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

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

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

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

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

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

npm init -y

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

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

npm install @azure/communication-common --save

npm install @azure/communication-identity --save

npm install @azure/communication-chat --save

npm install @azure/communication-calling --save

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

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

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

npm install webpack@5.89.0 webpack-cli@5.1.4 webpack-dev-server@4.15.1 --save-dev

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

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

Замените код в файле index.html приведенным ниже фрагментом кода. Текстовое поле в верхней части страницы будет использоваться для ввода контекста собрания Teams. Кнопка "Присоединиться к собранию Teams" используется для присоединения к указанному собранию. Всплывающее окно чата отображается в нижней части страницы. Его можно использовать для отправки сообщений в потоке собраний, и он отображается в режиме реального времени любые сообщения, отправленные в потоке, пока пользователь служб коммуникации является членом.

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

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

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

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

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

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

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

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

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

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

  const identityClient = new CommunicationIdentityClient(connectionString);

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

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

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

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

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

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

init();

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

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

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

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

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

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

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

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

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

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

  console.log(call);
});

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Примечание.

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

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

Пример кода

Если вы хотите сразу перейти к завершающему этапу, можно скачать это краткое руководство в качестве примера с портала GitHub.

Необходимые компоненты

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

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

Установка

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

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

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

Установка CocoaPods

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

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

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

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

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

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

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

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

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

В целевом объекте выберите вкладку Info и добавьте строку для параметра "Конфиденциальность — описание использования микрофона"

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

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

Некоторые скрипты в связанных библиотеках записывают файлы во время сборки. Чтобы разрешить это, отключите песочницу пользовательского скрипта в Xcode. В параметрах сборки найдите sandbox и установите значение User Script SandboxingNo.

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

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

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

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

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

import AVFoundation
import SwiftUI

import AzureCommunicationCalling
import AzureCommunicationChat

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

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

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

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

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

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

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

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

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

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

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

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

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

      .navigationBarTitle("Teams Chat Interop")
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    default:
      break
    }
  }

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

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

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

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

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

Выход из чата

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

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

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

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

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

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

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

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

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

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

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

Примечание.

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

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

Пример кода

Если вы хотите сразу перейти к завершающему этапу, можно скачать это краткое руководство в качестве примера с портала GitHub.

Необходимые компоненты

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

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

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

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

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

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

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

Внимание

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

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

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

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

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

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

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

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

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

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

    </LinearLayout>

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

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

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

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

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

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

</androidx.constraintlayout.widget.ConstraintLayout>

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

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

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

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

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

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

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

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

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

Измените конец метода MainActivity.joinTeamsMeeting(), используя приведенный ниже код:

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

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

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

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

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

Внимание

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

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

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

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

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

Обратите внимание на то, что эта задача уже запускается в конце метода MainActivity.joinTeamsMeeting(), обновленного на этапе инициализации.

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

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

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

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

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

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

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

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

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

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

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

Примечание.

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

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

Пример кода

Код для этого краткого руководства можно найти на сайте GitHub.

Необходимые компоненты

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

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

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

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

  1. Откройте экземпляр PowerShell, Терминал Windows, командную строку или эквивалентную команду и перейдите к каталогу, в который вы хотите клонировать пример.
  2. git clone https://github.com/Azure-Samples/Communication-Services-dotnet-quickstarts.git
  3. Откройте проект ChatTeamsInteropQuickStart/ChatTeamsInteropQuickStart.csproj в Visual Studio.
  4. Установите следующие или более поздние версии пакетов NuGet:
Install-Package Azure.Communication.Calling -Version 1.0.0-beta.29
Install-Package Azure.Communication.Chat -Version 1.1.0
Install-Package Azure.Communication.Common -Version 1.0.1
Install-Package Azure.Communication.Identity -Version 1.0.1

  1. При использовании ресурса служб коммуникации, приобретенного в предварительных требованиях, добавьте строку подключения в файл ChatTeamsInteropQuickStart/MainPage.xaml.cs .
//Azure Communication Services resource connection string, i.e., = "endpoint=https://your-resource.communication.azure.net/;accesskey=your-access-key";
private const string connectionString_ = "";

Внимание

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

Если эти настройки заданы неправильно, следующие шаги не удастся выполнить.

  1. Нажмите клавишу F5, чтобы запустить проект в режиме отладки.
  2. Вставьте допустимую ссылку на собрание Teams в соответствующем поле (см. следующий раздел).
  3. Нажмите кнопку "Присоединиться к собранию в Teams", чтобы начать беседу.

Внимание

Когда пакет SDK для звонков установит подключение к собранию Teams (см. раздел о приложении Windows для вызовов с помощью Служб коммуникации), ключевыми функциями для управления операциями чата будут StartPollingForChatMessages и SendMessageButton_Click. Оба фрагмента кода находятся в ChatTeamsInteropQuickStart\MainPage.xaml.cs.

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

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

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

                        }
                        if (!keepPolling_)
                        {
                            return;
                        }

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

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

Вы также можете получить необходимую ссылку на собрание из URL-адреса Присоединиться к собранию в самом приглашении на собрание Teams. Ссылка на собрание в Teams выглядит следующим образом: https://teams.microsoft.com/l/meetup-join/meeting_chat_thread_id/1606337455313?context=some_context_here. Если у вашей команды есть другой формат, необходимо получить идентификатор потока с помощью API Graph.

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

Примечание.

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

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

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

Следующие шаги

Дополнительные сведения см. в следующих статьях: