Skip to content
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
26 changes: 13 additions & 13 deletions examples/graph/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from azure.core.exceptions import ClientAuthenticationError
from microsoft_teams.api import MessageActivity
from microsoft_teams.apps import ActivityContext, App, AppOptions, ErrorEvent, SignInEvent
from microsoft_teams.apps import ActivityContext, App, ErrorEvent, SignInEvent
from microsoft_teams.common import ConsoleFormatter
from microsoft_teams.graph import get_graph_client
from msgraph.generated.users.item.messages.messages_request_builder import ( # type: ignore
Expand All @@ -23,8 +23,8 @@
logging.getLogger().addHandler(stream_handler)
logger = logging.getLogger(__name__)

app_options = AppOptions(default_connection_name=os.getenv("CONNECTION_NAME", "graph"))
app = App(**app_options)
app = App()
graph_flow = app.add_oauth_flow(os.getenv("CONNECTION_NAME", "graph"))


async def get_authenticated_graph_client(ctx: ActivityContext[MessageActivity]):
Expand All @@ -34,40 +34,40 @@ async def get_authenticated_graph_client(ctx: ActivityContext[MessageActivity]):
Returns:
Graph client if successful, None if authentication failed.
"""
# Check if user is signed in
if not ctx.is_signed_in:
token = await graph_flow.get_token(ctx)
if token is None:
await ctx.send("🔐 Please sign in first to access Microsoft Graph.")
await ctx.sign_in()
await graph_flow.sign_in(ctx)
return None

try:
# Create Graph client using the user token
return get_graph_client(ctx.user_token)
return get_graph_client(token)

except Exception as e:
logger.error(f"Failed to create Graph client: {e}")
await ctx.send("🔐 Failed to create authenticated client. Please try signing in again.")
await ctx.sign_in()
await graph_flow.sign_in(ctx)
return None


@app.on_message_pattern("signin")
async def handle_signin_command(ctx: ActivityContext[MessageActivity]):
"""Handle sign-in command."""
if ctx.is_signed_in:
if await graph_flow.is_signed_in(ctx):
await ctx.send("✅ You are already signed in!")
else:
await ctx.send("🔐 Please sign in to access Microsoft Graph...")
await ctx.sign_in()
await graph_flow.sign_in(ctx)


@app.on_message_pattern("signout")
async def handle_signout_command(ctx: ActivityContext[MessageActivity]):
"""Handle sign-out command."""
if not ctx.is_signed_in:
if not await graph_flow.is_signed_in(ctx):
await ctx.send("ℹ️ You are not currently signed in.")
else:
await ctx.sign_out()
await graph_flow.sign_out(ctx)
await ctx.send("👋 You have been signed out successfully!")


Expand Down Expand Up @@ -98,7 +98,7 @@ async def handle_profile_command(ctx: ActivityContext[MessageActivity]):
except ClientAuthenticationError as e:
logger.error(f"Authentication error: {e}")
await ctx.send("🔐 Authentication failed. Please try signing in again.")
await ctx.sign_in()
await graph_flow.sign_in(ctx)

except Exception as e:
logger.error(f"Error getting profile: {e}")
Expand Down
2 changes: 2 additions & 0 deletions examples/oauth/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ requires-python = ">=3.11,<4.0"
dependencies = [
"dotenv>=0.9.9",
"microsoft-teams-apps",
"microsoft-teams-graph",
]

[tool.uv.sources]
microsoft-teams-apps = { workspace = true }
microsoft-teams-graph = { workspace = true }
109 changes: 80 additions & 29 deletions examples/oauth/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,55 +7,106 @@
import logging

from microsoft_teams.api import MessageActivity
from microsoft_teams.api.activities.invoke.sign_in import SignInFailureInvokeActivity
from microsoft_teams.apps import ActivityContext, App, SignInEvent
from microsoft_teams.apps.events.types import ErrorEvent
from microsoft_teams.apps.events.types import ErrorEvent, SignInFailureEvent
from microsoft_teams.common import ConsoleFormatter
from microsoft_teams.graph import get_graph_client

# Setup logging
logging.getLogger().setLevel(logging.DEBUG)
logging.getLogger().setLevel(logging.INFO)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(ConsoleFormatter())
logging.getLogger().addHandler(stream_handler)
logger = logging.getLogger(__name__)

app = App()
# Pending sign-in hints are stored in per-turn state, which lets connection-less
# callbacks be routed back to the flow that started them.
app = App(state=True)

# Multi-connection OAuth: two named connections registered with add_oauth_flow, each
# driven through the OAuthFlow it returns.
#
# Bot needs two OAuth connections configured in Azure, named "profile" and
# "mail", matching the names passed below. Grant "User.Read" to the first and
# "Mail.Read" to the second.
profile = app.add_oauth_flow("profile", oauth_card_text="Sign in to read your profile")
mail = app.add_oauth_flow("mail", oauth_card_text="Sign in to read your mail")


@profile.on_signin
async def on_profile_signin(event: SignInEvent) -> None:
"""Only fires for the `profile` connection, using that connection's token."""
client = get_graph_client(event.token_response.token)
me = await client.me.get()
name = me.display_name if me else "unknown"
await event.activity_ctx.send(f"Signed in as **{name}**.")


@mail.on_signin
async def on_mail_signin(event: SignInEvent) -> None:
"""Only fires for the `mail` connection — a token the profile flow does not have."""
client = get_graph_client(event.token_response.token)
page = await client.me.messages.get()
subjects = [m.subject or "(no subject)" for m in (page.value or [])[:3]] if page else []
body = "\n".join(f"- {s}" for s in subjects) if subjects else "_no messages_"
await event.activity_ctx.send(f"Latest mail:\n{body}")


@profile.on_signin_failure
async def on_profile_failure(event: SignInFailureEvent) -> None:
await event.activity_ctx.send(f"Profile sign-in failed: {event.code} - {event.message}")

@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]):
"""Handle message activities using the new generated handler system."""
print(f"[GENERATED onMessage] Message received: {ctx.activity.text}")
print(f"[GENERATED onMessage] From: {ctx.activity.from_}")

logger.info("User requested sign-in.")
if ctx.is_signed_in:
await ctx.send("You are already signed in. Logging you out.")
await ctx.sign_out()
else:
await ctx.sign_in()
@mail.on_signin_failure
async def on_mail_failure(event: SignInFailureEvent) -> None:
await event.activity_ctx.send(f"Mail sign-in failed: {event.code} - {event.message}")


@app.event("sign_in")
async def handle_sign_in(event: SignInEvent):
"""Handle sign-in events."""
await event.activity_ctx.send("You are now signed in!")
async def on_any_signin(event: SignInEvent) -> None:
"""Fires for every connection. `connection_name` says which one completed."""
logger.info("sign-in completed on connection %r", event.connection_name)


@app.on_message
async def handle_message(ctx: ActivityContext[MessageActivity]) -> None:
# The bot is @mentioned in group chats and channels, so drop the mention
# before dispatching to keep the same commands working in every scope.
text = (ctx.activity.strip_mentions_text().text or "").strip().lower()

if text == "sign in profile":
# Returns the token directly when one is already cached, otherwise sends a
# card and returns None; the flow's on_signin handler fires once it completes.
if await profile.sign_in(ctx):
await ctx.send("Already signed in for profile access.")
return

if text == "sign in mail":
if await mail.sign_in(ctx):
await ctx.send("Already signed in for mail access.")
return

if text == "sign out":
await profile.sign_out(ctx)
await mail.sign_out(ctx)
await ctx.send("Signed out of both connections.")
return

if text == "status":
# Tokens are tracked per connection, so these can differ.
lines: list[str] = []
for flow in (profile, mail):
state = "signed in" if await flow.is_signed_in(ctx) else "signed out"
lines.append(f"- `{flow.connection_name}`: {state}")
await ctx.send("\n".join(lines))
return

@app.on_signin_failure()
async def handle_signin_failure(ctx: ActivityContext[SignInFailureInvokeActivity]):
"""Handle sign-in failure events."""
failure = ctx.activity.value
print(f"Sign-in failed: {failure.code} - {failure.message}")
await ctx.send("Sign-in failed.")
await ctx.send("Try `sign in profile`, `sign in mail`, `status`, or `sign out`.")


@app.event("error")
async def handle_error(event: ErrorEvent):
"""Handle error events."""
print(f"Error occurred: {event.error}")
if event.context:
print(f"Context: {event.context}")
async def handle_error(event: ErrorEvent) -> None:
logger.error("error: %s (context=%s)", event.error, event.context)


if __name__ == "__main__":
Expand Down
Loading