Skip to content

Josh/teams activitiy handler #452

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
ConversationAccount,
ConversationParameters,
ConversationReference,
ResourceResponse,
TokenResponse,
ResourceResponse,
)
Expand All @@ -39,7 +38,6 @@
OAUTH_ENDPOINT = "https://api.botframework.com"
US_GOV_OAUTH_ENDPOINT = "https://api.botframework.azure.us"


class TokenExchangeState(Model):
_attribute_map = {
"connection_name": {"key": "connectionName", "type": "str"},
Expand Down Expand Up @@ -332,7 +330,7 @@ async def delete_activity(
except Exception as error:
raise error

async def send_activities(
async def send_activities(
self, context: TurnContext, activities: List[Activity]
) -> List[ResourceResponse]:
try:
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@

from botbuilder.core import MemoryStorage
from botbuilder.schema import Activity


class ActivityLog():
class ActivityLog:
def __init__(self, storage: MemoryStorage):
self._storage = storage

async def append(self, activity_id: str, activity: Activity):
if not activity_id:
raise TypeError("activity_id is required for ActivityLog.append")

if not activity:
raise TypeError("activity is required for ActivityLog.append")

obj = {}
obj[activity_id] = activity

Expand All @@ -23,6 +22,6 @@ async def append(self, activity_id: str, activity: Activity):
async def find(self, activity_id: str) -> Activity:
if not activity_id:
raise TypeError("activity_id is required for ActivityLog.find")

items = await self._storage.read([activity_id])
return items[activity_id] if len(items) >= 1 else None
return items[activity_id] if len(items) >= 1 else None
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
BotFrameworkAdapterSettings,
TurnContext,
BotFrameworkAdapter,
MemoryStorage
MemoryStorage,
)
from botbuilder.schema import Activity, ActivityTypes
from activity_log import ActivityLog
Expand All @@ -28,7 +28,7 @@


# Catch-all for errors.
async def on_error( # pylint: disable=unused-argument
async def on_error( # pylint: disable=unused-argument
self, context: TurnContext, error: Exception
):
# This check writes out errors to console log .vs. app insights.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,60 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from botbuilder.core import MessageFactory, TurnContext
from botbuilder.core import ActivityHandler
from botbuilder.schema import MessageReaction
from typing import List
from botbuilder.core import MessageFactory, TurnContext, ActivityHandler
from botbuilder.schema import MessageReaction
from activity_log import ActivityLog


class MessageReactionBot(ActivityHandler):
def __init__(self, activity_log: ActivityLog):
self._log = activity_log

async def on_reactions_added(self, message_reactions: List[MessageReaction], turn_context: TurnContext):
async def on_reactions_added(
self, message_reactions: List[MessageReaction], turn_context: TurnContext
):
for reaction in message_reactions:
activity = await self._log.find(turn_context.activity.reply_to_id)
if not activity:
await self._send_message_and_log_activity_id(turn_context, f"Activity {turn_context.activity.reply_to_id} not found in log")
await self._send_message_and_log_activity_id(
turn_context,
f"Activity {turn_context.activity.reply_to_id} not found in log",
)
else:
await self._send_message_and_log_activity_id(turn_context, f"You added '{reaction.type}' regarding '{activity.text}'")
return

async def on_reactions_removed(self, message_reactions: List[MessageReaction], turn_context: TurnContext):
await self._send_message_and_log_activity_id(
turn_context,
f"You added '{reaction.type}' regarding '{activity.text}'",
)
return

async def on_reactions_removed(
self, message_reactions: List[MessageReaction], turn_context: TurnContext
):
for reaction in message_reactions:
activity = await self._log.find(turn_context.activity.reply_to_id)
if not activity:
await self._send_message_and_log_activity_id(turn_context, f"Activity {turn_context.activity.reply_to_id} not found in log")
await self._send_message_and_log_activity_id(
turn_context,
f"Activity {turn_context.activity.reply_to_id} not found in log",
)
else:
await self._send_message_and_log_activity_id(turn_context, f"You removed '{reaction.type}' regarding '{activity.text}'")
await self._send_message_and_log_activity_id(
turn_context,
f"You removed '{reaction.type}' regarding '{activity.text}'",
)
return

async def on_message_activity(self, turn_context: TurnContext):
await self._send_message_and_log_activity_id(turn_context, f"echo: {turn_context.activity.text}")

async def _send_message_and_log_activity_id(self, turn_context: TurnContext, text: str):
await self._send_message_and_log_activity_id(
turn_context, f"echo: {turn_context.activity.text}"
)

async def _send_message_and_log_activity_id(
self, turn_context: TurnContext, text: str
):
reply_activity = MessageFactory.text(text)
resource_response = await turn_context.send_activity(reply_activity)

await self._log.append(resource_response.id, reply_activity)
return
return
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def _do_waitpid(self, loop, expected_pid, callback, args):

logger = logging.getLogger(__name__)


class EventLoopThread(threading.Thread):
loop = None
_count = itertools.count(0)
Expand Down Expand Up @@ -142,9 +143,11 @@ def stop(self):
loop.call_soon_threadsafe(loop.stop)
self.join()


_lock = threading.Lock()
_loop_thread = None


def get_event_loop():
global _loop_thread
with _lock:
Expand All @@ -153,12 +156,14 @@ def get_event_loop():
_loop_thread.start()
return _loop_thread.loop


def stop_event_loop():
global _loop_thread
with _lock:
if _loop_thread is not None:
_loop_thread.stop()
_loop_thread = None


def run_coroutine(coro):
return asyncio.run_coroutine_threadsafe(coro, get_event_loop())
100 changes: 100 additions & 0 deletions libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from typing import List

import aiounittest
from botbuilder.core import BotAdapter, TurnContext
from botbuilder.core.teams import TeamsActivityHandler
from botbuilder.schema import (
Activity,
ActivityTypes,
ChannelAccount,
ConversationReference,
MessageReaction,
ResourceResponse,
)


class TestingTeamsActivityHandler(TeamsActivityHandler):
def __init__(self):
self.record: List[str] = []

async def on_message_activity(self, turn_context: TurnContext):
self.record.append("on_message_activity")
return await super().on_message_activity(turn_context)

async def on_members_added_activity(
self, members_added: ChannelAccount, turn_context: TurnContext
):
self.record.append("on_members_added_activity")
return await super().on_members_added_activity(members_added, turn_context)

async def on_members_removed_activity(
self, members_removed: ChannelAccount, turn_context: TurnContext
):
self.record.append("on_members_removed_activity")
return await super().on_members_removed_activity(members_removed, turn_context)

async def on_message_reaction_activity(self, turn_context: TurnContext):
self.record.append("on_message_reaction_activity")
return await super().on_message_reaction_activity(turn_context)

async def on_reactions_added(
self, message_reactions: List[MessageReaction], turn_context: TurnContext
):
self.record.append("on_reactions_added")
return await super().on_reactions_added(message_reactions, turn_context)

async def on_reactions_removed(
self, message_reactions: List[MessageReaction], turn_context: TurnContext
):
self.record.append("on_reactions_removed")
return await super().on_reactions_removed(message_reactions, turn_context)

async def on_token_response_event(self, turn_context: TurnContext):
self.record.append("on_token_response_event")
return await super().on_token_response_event(turn_context)

async def on_event(self, turn_context: TurnContext):
self.record.append("on_event")
return await super().on_event(turn_context)

async def on_unrecognized_activity_type(self, turn_context: TurnContext):
self.record.append("on_unrecognized_activity_type")
return await super().on_unrecognized_activity_type(turn_context)


class NotImplementedAdapter(BotAdapter):
async def delete_activity(
self, context: TurnContext, reference: ConversationReference
):
raise NotImplementedError()

async def send_activities(
self, context: TurnContext, activities: List[Activity]
) -> List[ResourceResponse]:
raise NotImplementedError()

async def update_activity(self, context: TurnContext, activity: Activity):
raise NotImplementedError()


class TestTeamsActivityHandler(aiounittest.AsyncTestCase):
async def test_message_reaction(self):
# Note the code supports multiple adds and removes in the same activity though
# a channel may decide to send separate activities for each. For example, Teams
# sends separate activities each with a single add and a single remove.

# Arrange
activity = Activity(
type=ActivityTypes.message_reaction,
reactions_added=[MessageReaction(type="sad")],
)
turn_context = TurnContext(NotImplementedAdapter(), activity)

# Act
bot = TestingTeamsActivityHandler()
await bot.on_turn(turn_context)

# Assert
assert len(bot.record) == 2
assert bot.record[0] == "on_message_reaction_activity"
assert bot.record[1] == "on_reactions_added"
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class ActivityTypes(str, Enum):
end_of_conversation = "endOfConversation"
event = "event"
invoke = "invoke"
invoke_response = "invokeResponse"
delete_user_data = "deleteUserData"
message_update = "messageUpdate"
message_delete = "messageDelete"
Expand Down