-
Notifications
You must be signed in to change notification settings - Fork 268
Support new shorter Teams meeting URLs #856
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. " + | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
| throw new ArgumentException($"Join URL cannot be parsed: {joinURL}.", nameof(joinURL)); | ||
| } | ||
|
|
||
|
|
||
| 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); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
|
||
| throw new ArgumentException($"Join URL cannot be parsed: {joinURL}.", nameof(joinURL)); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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(). | ||||||
|
||||||
| /// 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
AI
Feb 19, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.