Skip to content
Open
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
24 changes: 23 additions & 1 deletion Samples/Common/Sample.Common.Beta/Meetings/JoinInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,42 @@ public class JoinInfo
{
/// <summary>
/// Parse Join URL into its components.
/// NOTE: This method only works with the OLD Teams meeting URL format that includes a context parameter.
/// For NEW shorter Teams meeting URLs (introduced with MCnumber rollout), you should:
/// 1. Use the Graph API to query the OnlineMeeting by JoinWebUrl to get meeting details
/// 2. Use JoinMeetingIdMeetingInfo with the meeting ID and passcode from the API response
/// Example: var meeting = await graphClient.Communications.OnlineMeetings.Request().Filter($"JoinWebUrl eq '{encodedUrl}'").GetAsync();
/// </summary>
/// <param name="joinURL">Join URL from Team's meeting body.</param>
/// <returns>Parsed data.</returns>
public static (ChatInfo, MeetingInfo) ParseJoinURL(string joinURL)
{
if (string.IsNullOrWhiteSpace(joinURL))
{
throw new ArgumentException($"Join URL cannot be null, empty, or whitespace: {joinURL}", nameof(joinURL));
}

var decodedURL = WebUtility.UrlDecode(joinURL);

//// URL being needs to be in this format.
//// Old URL format with context parameter:
//// https://teams.microsoft.com/l/meetup-join/19:cd9ce3da56624fe69c9d7cd026f9126d@thread.skype/1509579179399?context={"Tid":"72f988bf-86f1-41af-91ab-2d7cd011db47","Oid":"550fae72-d251-43ec-868c-373732c2704f","MessageId":"1536978844957"}
//// New shorter URL format (not supported by this parser):
//// https://teams.microsoft.com/l/meetup-join/...

var regex = new Regex("https://teams\\.microsoft\\.com.*/(?<thread>[^/]+)/(?<message>[^/]+)\\?context=(?<context>{.*})");
var match = regex.Match(decodedURL);
if (!match.Success)
{
// Check if this is a new shorter URL format
if (decodedURL.Contains("teams.microsoft.com") && decodedURL.Contains("/meetup-join/") && !decodedURL.Contains("?context="))
{
throw new NotSupportedException(
$"This appears to be a new shorter Teams meeting URL format which is not supported by this parser. " +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if we should have the steps to resolve the short url in the exception or move it to a readme file and link it here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can create a separate issue to code the short url resolver which can be added to Common and referenced by sample bot applications.

$"To join meetings with this URL format, please use the Graph API to resolve the meeting details:\n" +
$"1. Query: GET /communications/onlineMeetings?$filter=JoinWebUrl eq '{Uri.EscapeDataString(joinURL)}'\n" +
$"2. Use JoinMeetingIdMeetingInfo with the meeting.JoinMeetingIdSettings from the response.\n" +
$"See: https://learn.microsoft.com/graph/api/resources/joinmeetingidmeetinginfo");
Comment on lines +55 to +58

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This info resolution logic should be documented in... the docs. And then be referred to from here

}
Comment on lines +50 to +59
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The detection logic for new shorter URLs is overly broad and may produce false positives. Any Teams URL without "?context=" will be classified as the new format, even if it's simply a malformed or different type of Teams URL. Consider making the detection more specific, perhaps by checking for specific patterns in the new format or by having a more explicit check before throwing NotSupportedException.

Copilot uses AI. Check for mistakes.
throw new ArgumentException($"Join URL cannot be parsed: {joinURL}.", nameof(joinURL));
}

Expand Down
6 changes: 3 additions & 3 deletions Samples/Common/Sample.Common.Tests/HeartbeatHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class HeartbeatHandlerTests
/// <summary>
/// Gets or sets the test context.
/// </summary>
public TestContext TestContext
public TestContext? TestContext
{
get; set;
}
Expand Down Expand Up @@ -73,7 +73,7 @@ public void HeartbeatShouldLogSuccess()
onNext: @event =>
{
Interlocked.Increment(ref loggerCount);
this.TestContext.WriteLine(formatter.Format(@event));
this.TestContext!.WriteLine(formatter.Format(@event));
},
onError: @exception =>
{
Expand Down Expand Up @@ -107,7 +107,7 @@ public void HeartbeatShouldLogFailure()
onNext: @event =>
{
Interlocked.Increment(ref errorCount);
this.TestContext.WriteLine(formatter.Format(@event));
this.TestContext!.WriteLine(formatter.Format(@event));
},
onError: @exception =>
{
Expand Down
187 changes: 187 additions & 0 deletions Samples/Common/Sample.Common.Tests/Meetings/JoinInfoTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// <copyright file="JoinInfoTests.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
// </copyright>

namespace Samples.Common.Tests.Meetings
{
using System;
using Microsoft.Graph;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Sample.Common.Meetings;

/// <summary>
/// Unit tests for JoinInfo URL parsing.
/// </summary>
[TestClass]
public class JoinInfoTests
{
private const string OldFormatUrl = "https://teams.microsoft.com/l/meetup-join/19:cd9ce3da56624fe69c9d7cd026f9126d@thread.skype/1509579179399?context={\"Tid\":\"72f988bf-86f1-41af-91ab-2d7cd011db47\",\"Oid\":\"550fae72-d251-43ec-868c-373732c2704f\",\"MessageId\":\"1536978844957\"}";
private const string OldFormatUrlEncoded = "https://teams.microsoft.com/l/meetup-join/19:cd9ce3da56624fe69c9d7cd026f9126d@thread.skype/1509579179399?context=%7B%22Tid%22%3A%2272f988bf-86f1-41af-91ab-2d7cd011db47%22%2C%22Oid%22%3A%22550fae72-d251-43ec-868c-373732c2704f%22%2C%22MessageId%22%3A%221536978844957%22%7D";
private const string NewFormatUrlShort = "https://teams.microsoft.com/l/meetup-join/19:meeting_abc123def456@thread.v2/0";
private const string NewFormatUrlWithoutContext = "https://teams.microsoft.com/l/meetup-join/19:meeting_YzY4NDFjZDUtZTdlOC00MDg3LWI3M2QtZTgzYzdiYzM0Yjk5@thread.skype/0";

/// <summary>
/// Gets or sets the test context.
/// </summary>
public TestContext? TestContext { get; set; }

/// <summary>
/// Test that old format URL with context parameter parses successfully.
/// </summary>
[TestMethod]
public void ParseJoinURL_OldFormatWithContext_ParsesSuccessfully()
{
// Act
var (chatInfo, meetingInfo) = JoinInfo.ParseJoinURL(OldFormatUrl);

// Assert
Assert.IsNotNull(chatInfo);
Assert.IsNotNull(meetingInfo);
Assert.AreEqual("19:cd9ce3da56624fe69c9d7cd026f9126d@thread.skype", chatInfo.ThreadId);
Assert.AreEqual("1509579179399", chatInfo.MessageId);
Assert.AreEqual("1536978844957", chatInfo.ReplyChainMessageId);

Assert.IsInstanceOfType(meetingInfo, typeof(OrganizerMeetingInfo));
var organizerInfo = (OrganizerMeetingInfo)meetingInfo;
Assert.IsNotNull(organizerInfo.Organizer);
Assert.IsNotNull(organizerInfo.Organizer.User);
Assert.AreEqual("550fae72-d251-43ec-868c-373732c2704f", organizerInfo.Organizer.User.Id);
}

/// <summary>
/// Test that old format URL with encoded context parameter parses successfully.
/// </summary>
[TestMethod]
public void ParseJoinURL_OldFormatUrlEncoded_ParsesSuccessfully()
{
// Act
var (chatInfo, meetingInfo) = JoinInfo.ParseJoinURL(OldFormatUrlEncoded);

// Assert
Assert.IsNotNull(chatInfo);
Assert.IsNotNull(meetingInfo);
Assert.AreEqual("19:cd9ce3da56624fe69c9d7cd026f9126d@thread.skype", chatInfo.ThreadId);
Assert.IsInstanceOfType(meetingInfo, typeof(OrganizerMeetingInfo));
}

/// <summary>
/// Test that new shorter URL format throws NotSupportedException.
/// </summary>
[TestMethod]
[ExpectedException(typeof(NotSupportedException))]
public void ParseJoinURL_NewShorterFormat_ThrowsNotSupportedException()
{
// Act
JoinInfo.ParseJoinURL(NewFormatUrlShort);
}

/// <summary>
/// Test that new URL format without context throws NotSupportedException.
/// </summary>
[TestMethod]
[ExpectedException(typeof(NotSupportedException))]
public void ParseJoinURL_NewFormatWithoutContext_ThrowsNotSupportedException()
{
// Act
JoinInfo.ParseJoinURL(NewFormatUrlWithoutContext);
}

/// <summary>
/// Test that new format URL exception message contains helpful guidance.
/// </summary>
[TestMethod]
public void ParseJoinURL_NewFormat_ExceptionContainsGuidance()
{
// Act & Assert
var exception = Assert.ThrowsException<NotSupportedException>(() =>
{
JoinInfo.ParseJoinURL(NewFormatUrlShort);
});

this.TestContext!.WriteLine($"Exception message: {exception.Message}");

// Verify the exception message contains helpful guidance
Assert.IsTrue(exception.Message.Contains("new shorter Teams meeting URL format"));
Assert.IsTrue(exception.Message.Contains("Graph API"));
Assert.IsTrue(exception.Message.Contains("JoinMeetingIdMeetingInfo"));
Assert.IsTrue(exception.Message.Contains("/communications/onlineMeetings"));
}

/// <summary>
/// Test that null URL throws ArgumentException.
/// </summary>
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ParseJoinURL_NullUrl_ThrowsArgumentException()
{
// Act
JoinInfo.ParseJoinURL(null);
}

/// <summary>
/// Test that empty URL throws ArgumentException.
/// </summary>
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ParseJoinURL_EmptyUrl_ThrowsArgumentException()
{
// Act
JoinInfo.ParseJoinURL(string.Empty);
}

/// <summary>
/// Test that whitespace URL throws ArgumentException.
/// </summary>
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ParseJoinURL_WhitespaceUrl_ThrowsArgumentException()
{
// Act
JoinInfo.ParseJoinURL(" ");
}

/// <summary>
/// Test that completely invalid URL throws ArgumentException.
/// </summary>
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ParseJoinURL_InvalidUrl_ThrowsArgumentException()
{
// Act
JoinInfo.ParseJoinURL("https://example.com/not-a-teams-url");
}

/// <summary>
/// Test that malformed Teams URL throws ArgumentException.
/// </summary>
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ParseJoinURL_MalformedTeamsUrl_ThrowsArgumentException()
{
// Act
JoinInfo.ParseJoinURL("https://teams.microsoft.com/malformed?context=invalid");
}

/// <summary>
/// Test that different thread formats are parsed correctly.
/// </summary>
[TestMethod]
public void ParseJoinURL_DifferentThreadFormats_ParsesCorrectly()
{
// Arrange
var threadSkypeUrl = "https://teams.microsoft.com/l/meetup-join/19:meeting_abc@thread.skype/0?context={\"Tid\":\"72f988bf-86f1-41af-91ab-2d7cd011db47\",\"Oid\":\"550fae72-d251-43ec-868c-373732c2704f\"}";
var threadV2Url = "https://teams.microsoft.com/l/meetup-join/19:meeting_abc@thread.v2/0?context={\"Tid\":\"72f988bf-86f1-41af-91ab-2d7cd011db47\",\"Oid\":\"550fae72-d251-43ec-868c-373732c2704f\"}";

// Act
var (chatInfo1, meetingInfo1) = JoinInfo.ParseJoinURL(threadSkypeUrl);
var (chatInfo2, meetingInfo2) = JoinInfo.ParseJoinURL(threadV2Url);

// Assert
Assert.AreEqual("19:meeting_abc@thread.skype", chatInfo1.ThreadId);
Assert.AreEqual("19:meeting_abc@thread.v2", chatInfo2.ThreadId);
Assert.IsNotNull(meetingInfo1);
Assert.IsNotNull(meetingInfo2);
}
}
}
12 changes: 6 additions & 6 deletions Samples/Common/Sample.Common.Tests/Sample.Common.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>

<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>

<Import Project="$(MSBuildThisFileDirectory)..\..\Graph.props" />

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.0.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.0.0" />
<PackageReference Include="coverlet.collector" Version="1.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.5.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.5.2" />
<PackageReference Include="coverlet.collector" Version="6.0.2" />
</ItemGroup>

<ItemGroup>
Expand Down
24 changes: 23 additions & 1 deletion Samples/Common/Sample.Common.V1/Meetings/JoinInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,42 @@ public class JoinInfo
{
/// <summary>
/// Parse Join URL into its components.
/// NOTE: This method only works with the OLD Teams meeting URL format that includes a context parameter.
/// For NEW shorter Teams meeting URLs (introduced with MCnumber rollout), you should:
/// 1. Use the Graph API to query the OnlineMeeting by JoinWebUrl to get meeting details
/// 2. Use JoinMeetingIdMeetingInfo with the meeting ID and passcode from the API response
/// Example: var meeting = await graphClient.Communications.OnlineMeetings.Request().Filter($"JoinWebUrl eq '{encodedUrl}'").GetAsync();
/// </summary>
/// <param name="joinURL">Join URL from Team's meeting body.</param>
/// <returns>Parsed data.</returns>
public static (ChatInfo, MeetingInfo) ParseJoinURL(string joinURL)
{
if (string.IsNullOrWhiteSpace(joinURL))
{
throw new ArgumentException($"Join URL cannot be null, empty, or whitespace: {joinURL}", nameof(joinURL));
}

var decodedURL = WebUtility.UrlDecode(joinURL);

//// URL being needs to be in this format.
//// Old URL format with context parameter:
//// https://teams.microsoft.com/l/meetup-join/19:cd9ce3da56624fe69c9d7cd026f9126d@thread.skype/1509579179399?context={"Tid":"72f988bf-86f1-41af-91ab-2d7cd011db47","Oid":"550fae72-d251-43ec-868c-373732c2704f","MessageId":"1536978844957"}
//// New shorter URL format (not supported by this parser):
//// https://teams.microsoft.com/l/meetup-join/...

var regex = new Regex("https://teams\\.microsoft\\.com.*/(?<thread>[^/]+)/(?<message>[^/]+)\\?context=(?<context>{.*})");
var match = regex.Match(decodedURL);
if (!match.Success)
{
// Check if this is a new shorter URL format
if (decodedURL.Contains("teams.microsoft.com") && decodedURL.Contains("/meetup-join/") && !decodedURL.Contains("?context="))
{
throw new NotSupportedException(
$"This appears to be a new shorter Teams meeting URL format which is not supported by this parser. " +
$"To join meetings with this URL format, please use the Graph API to resolve the meeting details:\n" +
$"1. Query: GET /communications/onlineMeetings?$filter=JoinWebUrl eq '{Uri.EscapeDataString(joinURL)}'\n" +
$"2. Use JoinMeetingIdMeetingInfo with the meeting.JoinMeetingIdSettings from the response.\n" +
$"See: https://learn.microsoft.com/graph/api/resources/joinmeetingidmeetinginfo");
}
Comment on lines +50 to +59
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The detection logic for new shorter URLs is overly broad and may produce false positives. Any Teams URL without "?context=" will be classified as the new format, even if it's simply a malformed or different type of Teams URL. Consider making the detection more specific, perhaps by checking for specific patterns in the new format or by having a more explicit check before throwing NotSupportedException.

Copilot uses AI. Check for mistakes.
throw new ArgumentException($"Join URL cannot be parsed: {joinURL}.", nameof(joinURL));
}

Expand Down
25 changes: 24 additions & 1 deletion Samples/Common/Sample.Common/Meetings/JoinInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,43 @@ public class JoinInfo
{
/// <summary>
/// Parse Join URL into its components.
/// NOTE: This method only works with the OLD Teams meeting URL format that includes a context parameter.
/// For NEW shorter Teams meeting URLs (introduced with MCnumber rollout), you should:
/// 1. Use the Graph API to query the OnlineMeeting by JoinWebUrl to get meeting details.
/// 2. Use JoinMeetingIdMeetingInfo with the meeting ID and passcode from the API response.
/// Example: var meeting = await graphClient.Communications.OnlineMeetings.Request().Filter($"JoinWebUrl eq '{encodedUrl}'").GetAsync().
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation comment has an incomplete sentence at line 28. The line ends with "GetAsync()." without completing the code example. This should either end with a semicolon or provide additional context about what to do with the result.

Suggested change
/// Example: var meeting = await graphClient.Communications.OnlineMeetings.Request().Filter($"JoinWebUrl eq '{encodedUrl}'").GetAsync().
/// Example: var meeting = await graphClient.Communications.OnlineMeetings.Request().Filter($"JoinWebUrl eq '{encodedUrl}'").GetAsync();

Copilot uses AI. Check for mistakes.
/// </summary>
/// <param name="joinURL">Join URL from Team's meeting body.</param>
/// <returns>Parsed data.</returns>
public static (ChatInfo, MeetingInfo) ParseJoinURL(string joinURL)
{
if (string.IsNullOrWhiteSpace(joinURL))
{
throw new ArgumentException($"Join URL cannot be null, empty, or whitespace: {joinURL}", nameof(joinURL));
}

var decodedURL = WebUtility.UrlDecode(joinURL);

//// URL being needs to be in this format.
//// Old URL format with context parameter:
//// https://teams.microsoft.com/l/meetup-join/19:cd9ce3da56624fe69c9d7cd026f9126d@thread.skype/1509579179399?context={"Tid":"72f988bf-86f1-41af-91ab-2d7cd011db47","Oid":"550fae72-d251-43ec-868c-373732c2704f","MessageId":"1536978844957"}
//// New shorter URL format (not supported by this parser):
//// https://teams.microsoft.com/l/meetup-join/...

var regex = new Regex("https://teams\\.microsoft\\.com.*/(?<thread>[^/]+)/(?<message>[^/]+)\\?context=(?<context>{.*})");
var match = regex.Match(decodedURL);
if (!match.Success)
{
// Check if this is a new shorter URL format
if (decodedURL.Contains("teams.microsoft.com") && decodedURL.Contains("/meetup-join/") && !decodedURL.Contains("?context="))
{
throw new NotSupportedException(
$"This appears to be a new shorter Teams meeting URL format which is not supported by this parser. " +
$"To join meetings with this URL format, please use the Graph API to resolve the meeting details:\n" +
$"1. Query: GET /communications/onlineMeetings?$filter=JoinWebUrl eq '{Uri.EscapeDataString(joinURL)}'\n" +
$"2. Use JoinMeetingIdMeetingInfo with the meeting.JoinMeetingIdSettings from the response.\n" +
$"See: https://learn.microsoft.com/graph/api/resources/joinmeetingidmeetinginfo");
}
Comment on lines +50 to +59
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The detection logic for new shorter URLs is overly broad and may produce false positives. Any Teams URL without "?context=" will be classified as the new format, even if it's simply a malformed or different type of Teams URL. Consider making the detection more specific, perhaps by checking for specific patterns in the new format or by having a more explicit check before throwing NotSupportedException.

Copilot uses AI. Check for mistakes.

throw new ArgumentException($"Join URL cannot be parsed: {joinURL}.", nameof(joinURL));
}

Expand Down
Loading