Skip to content

Commit

Permalink
updated teams info for cloud adapter (#1776)
Browse files Browse the repository at this point in the history
* updated teams info for cloud adapter

* black corrections

---------

Co-authored-by: tracyboehrer <[email protected]>
Co-authored-by: Tracy Boehrer <[email protected]>
  • Loading branch information
3 people committed Nov 2, 2023
1 parent a202f94 commit 83bc043
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ async def continue_conversation(
reference, callback, bot_id, claims_identity, audience
)

async def create_conversation(
async def create_conversation( # pylint: disable=arguments-differ
self, channel_id: str, callback: Callable # pylint: disable=unused-argument
):
self.activity_buffer.clear()
Expand Down
50 changes: 48 additions & 2 deletions libraries/botbuilder-core/botbuilder/core/bot_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@

from abc import ABC, abstractmethod
from typing import List, Callable, Awaitable
from botbuilder.schema import Activity, ConversationReference, ResourceResponse
from botframework.connector.auth import ClaimsIdentity
from botbuilder.schema import (
Activity,
ConversationReference,
ConversationParameters,
ResourceResponse,
)
from botframework.connector.auth import AppCredentials, ClaimsIdentity

from . import conversation_reference_extension
from .bot_assert import BotAssert
Expand Down Expand Up @@ -108,6 +113,47 @@ async def continue_conversation(
)
return await self.run_pipeline(context, callback)

async def create_conversation(
self,
reference: ConversationReference,
logic: Callable[[TurnContext], Awaitable] = None,
conversation_parameters: ConversationParameters = None,
channel_id: str = None,
service_url: str = None,
credentials: AppCredentials = None,
):
"""
Starts a new conversation with a user. Used to direct message to a member of a group.
:param reference: The conversation reference that contains the tenant
:type reference: :class:`botbuilder.schema.ConversationReference`
:param logic: The logic to use for the creation of the conversation
:type logic: :class:`typing.Callable`
:param conversation_parameters: The information to use to create the conversation
:type conversation_parameters:
:param channel_id: The ID for the channel.
:type channel_id: :class:`typing.str`
:param service_url: The channel's service URL endpoint.
:type service_url: :class:`typing.str`
:param credentials: The application credentials for the bot.
:type credentials: :class:`botframework.connector.auth.AppCredentials`
:raises: It raises a generic exception error.
:return: A task representing the work queued to execute.
.. remarks::
To start a conversation, your bot must know its account information and the user's
account information on that channel.
Most channels only support initiating a direct message (non-group) conversation.
The adapter attempts to create a new conversation on the channel, and
then sends a conversation update activity through its middleware pipeline
to the the callback method.
If the conversation is established with the specified users, the ID of the activity
will contain the ID of the new conversation.
"""
raise Exception("Not Implemented")

async def run_pipeline(
self, context: TurnContext, callback: Callable[[TurnContext], Awaitable] = None
):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,11 @@ async def create_conversation(
)

# Mix in the tenant ID if specified. This is required for MS Teams.
if reference.conversation and reference.conversation.tenant_id:
if (
reference
and reference.conversation
and reference.conversation.tenant_id
):
# Putting tenant_id in channel_data is a temporary while we wait for the Teams API to be updated
if parameters.channel_data is None:
parameters.channel_data = {}
Expand Down
92 changes: 92 additions & 0 deletions libraries/botbuilder-core/botbuilder/core/cloud_adapter_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@
from copy import Error
from http import HTTPStatus
from typing import Awaitable, Callable, List, Union
from uuid import uuid4

from botbuilder.core.invoke_response import InvokeResponse

from botbuilder.schema import (
Activity,
ActivityEventNames,
ActivityTypes,
ConversationAccount,
ConversationReference,
ConversationResourceResponse,
ConversationParameters,
DeliveryModes,
ExpectedReplies,
ResourceResponse,
Expand Down Expand Up @@ -175,6 +180,71 @@ async def continue_conversation_with_claims(
claims_identity, get_continuation_activity(reference), audience, logic
)

async def create_conversation( # pylint: disable=arguments-differ
self,
bot_app_id: ConversationReference,
callback: Callable[[TurnContext], Awaitable] = None,
conversation_parameters: ConversationParameters = None,
channel_id: str = None,
service_url: str = None,
audience: str = None,
):
if not service_url:
raise TypeError(
"CloudAdapter.create_conversation(): service_url is required."
)
if not conversation_parameters:
raise TypeError(
"CloudAdapter.create_conversation(): conversation_parameters is required."
)
if not callback:
raise TypeError("CloudAdapter.create_conversation(): callback is required.")

# Create a ClaimsIdentity, to create the connector and for adding to the turn context.
claims_identity = self.create_claims_identity(bot_app_id)
claims_identity.claims[AuthenticationConstants.SERVICE_URL_CLAIM] = service_url

# create the connectror factory
connector_factory = self.bot_framework_authentication.create_connector_factory(
claims_identity
)

# Create the connector client to use for outbound requests.
connector_client = await connector_factory.create(service_url, audience)

# Make the actual create conversation call using the connector.
create_conversation_result = (
await connector_client.conversations.create_conversation(
conversation_parameters
)
)

# Create the create activity to communicate the results to the application.
create_activity = self._create_create_activity(
create_conversation_result, channel_id, service_url, conversation_parameters
)

# Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.)
user_token_client = (
await self.bot_framework_authentication.create_user_token_client(
claims_identity
)
)

# Create a turn context and run the pipeline.
context = self._create_turn_context(
create_activity,
claims_identity,
None,
connector_client,
user_token_client,
callback,
connector_factory,
)

# Run the pipeline
await self.run_pipeline(context, callback)

async def process_proactive(
self,
claims_identity: ClaimsIdentity,
Expand Down Expand Up @@ -301,6 +371,28 @@ def create_claims_identity(self, bot_app_id: str = "") -> ClaimsIdentity:
True,
)

def _create_create_activity(
self,
create_conversation_result: ConversationResourceResponse,
channel_id: str,
service_url: str,
conversation_parameters: ConversationParameters,
) -> Activity:
# Create a conversation update activity to represent the result.
activity = Activity.create_event_activity()
activity.name = ActivityEventNames.create_conversation
activity.channel_id = channel_id
activity.service_url = service_url
activity.id = create_conversation_result.activity_id or str(uuid4())
activity.conversation = ConversationAccount(
id=create_conversation_result.id,
tenant_id=conversation_parameters.tenant_id,
)
activity.channel_data = conversation_parameters.channel_data
activity.recipient = conversation_parameters.bot

return activity

def _create_turn_context(
self,
activity: Activity,
Expand Down
59 changes: 54 additions & 5 deletions libraries/botbuilder-core/botbuilder/core/teams/teams_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@

from typing import List, Tuple

from botframework.connector import Channels
from botframework.connector.aio import ConnectorClient
from botframework.connector.teams.teams_connector_client import TeamsConnectorClient
from botbuilder.schema import ConversationParameters, ConversationReference
from botframework.connector.teams import TeamsConnectorClient
from botbuilder.core.teams.teams_activity_extensions import (
teams_get_meeting_info,
teams_get_channel_data,
)
from botbuilder.core.turn_context import Activity, TurnContext
from botbuilder.core import CloudAdapterBase, BotFrameworkAdapter, TurnContext
from botbuilder.schema import Activity, ConversationParameters, ConversationReference
from botbuilder.schema.teams import (
ChannelInfo,
MeetingInfo,
Expand All @@ -25,23 +26,71 @@
class TeamsInfo:
@staticmethod
async def send_message_to_teams_channel(
turn_context: TurnContext, activity: Activity, teams_channel_id: str
turn_context: TurnContext,
activity: Activity,
teams_channel_id: str,
*,
bot_app_id: str = None,
) -> Tuple[ConversationReference, str]:
if not turn_context:
raise ValueError("The turn_context cannot be None")
if not turn_context.activity:
raise ValueError("The activity inside turn context cannot be None")
if not activity:
raise ValueError("The activity cannot be None")
if not teams_channel_id:
raise ValueError("The teams_channel_id cannot be None or empty")

if not bot_app_id:
return await TeamsInfo._legacy_send_message_to_teams_channel(
turn_context, activity, teams_channel_id
)

conversation_reference: ConversationReference = None
new_activity_id = ""
service_url = turn_context.activity.service_url
conversation_parameters = ConversationParameters(
is_group=True,
channel_data=TeamsChannelData(channel=ChannelInfo(id=teams_channel_id)),
activity=activity,
)

async def aux_callback(
new_turn_context,
):
nonlocal new_activity_id
nonlocal conversation_reference
new_activity_id = new_turn_context.activity.id
conversation_reference = TurnContext.get_conversation_reference(
new_turn_context.activity
)

adapter: CloudAdapterBase = turn_context.adapter
await adapter.create_conversation(
bot_app_id,
aux_callback,
conversation_parameters,
Channels.ms_teams,
service_url,
None,
)

return (conversation_reference, new_activity_id)

@staticmethod
async def _legacy_send_message_to_teams_channel(
turn_context: TurnContext, activity: Activity, teams_channel_id: str
) -> Tuple[ConversationReference, str]:
old_ref = TurnContext.get_conversation_reference(turn_context.activity)
conversation_parameters = ConversationParameters(
is_group=True,
channel_data={"channel": {"id": teams_channel_id}},
activity=activity,
)

result = await turn_context.adapter.create_conversation(
# if this version of the method is called the adapter probably wont be CloudAdapter
adapter: BotFrameworkAdapter = turn_context.adapter
result = await adapter.create_conversation(
old_ref, TeamsInfo._create_conversation_callback, conversation_parameters
)
return (result[0], result[1])
Expand Down
2 changes: 1 addition & 1 deletion libraries/botbuilder-core/tests/simple_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ async def send_activities(

return responses

async def create_conversation(
async def create_conversation( # pylint: disable=arguments-differ
self,
reference: ConversationReference,
logic: Callable[[TurnContext], Awaitable] = None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ async def send_activities(

return responses

async def create_conversation(
async def create_conversation( # pylint: disable=arguments-differ
self,
reference: ConversationReference,
logic: Callable[[TurnContext], Awaitable] = None,
Expand Down

0 comments on commit 83bc043

Please sign in to comment.