diff --git a/CHANGELOG.md b/CHANGELOG.md index a0e1e23c1..571906794 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - Added `New-PnPContainerType` cmdlet to create a new SharePoint container type. [#3669](https://github.com/pnp/powershell/pull/3669) - Added `Remove-PnPContainerType` cmdlet which removes a specific container type. [#3689](https://github.com/pnp/powershell/pull/3689/) - Added `Restore-PnPDeletedContainer` cmdlet which recovers a deleted Container from the Recycle Bin. [#3661](https://github.com/pnp/powershell/pull/3661) +- Added the ModerationSettings to be returned with `Get-PnPTeamsChannel` when passing in `-IncludeModerationSettings` and using `-Identity ` [#3580](https://github.com/pnp/powershell/pull/3580) +- Added `AllowNewMessageFromBots`, `AllowNewMessageFromConnectors`, `ReplyRestriction` and `UserNewMessageRestriction` to `Set-PnPTeamsChannel` which allows setting the moderation settings on a Teams channel [#3580](https://github.com/pnp/powershell/pull/3580) - Added `Get-PnPWebPermission` cmdlet which retrieves permission given by user for specific web. [#3685](https://github.com/pnp/powershell/pull/3685) - Added `-HorizontalQuickLaunch` parameter to `Set-PnPWeb` cmdlet to allow navigation orientation to be horizontal. [#3722](https://github.com/pnp/powershell/pull/3722) - Added `Set-PnPRetentionLabel` and `Reset-PnPRetentionLabel` cmdlets to support setting a retention label on one or more items [#3599](https://github.com/pnp/powershell/pull/3599) diff --git a/documentation/Get-PnPTeamsChannel.md b/documentation/Get-PnPTeamsChannel.md index 43959d693..dbba88d20 100644 --- a/documentation/Get-PnPTeamsChannel.md +++ b/documentation/Get-PnPTeamsChannel.md @@ -20,7 +20,7 @@ Gets the channels for a specified Team. ## SYNTAX ```powershell -Get-PnPTeamsChannel -Team [-Identity ] +Get-PnPTeamsChannel -Team [-Identity ] [-IncludeModerationSettings ] ``` @@ -28,6 +28,8 @@ Get-PnPTeamsChannel -Team [-Identity ] Allows to retrieve list of channels for a specified team. +Note that the ModerationSettings are only being returned when providing the channel Id of a specific channel through -Identity and by providing -IncludeModerationSettings (Example 4). They will not be returned when retrieving all channels for a team or when omitting -IncludeModerationSettings. This is because of a design choice in Microsoft Graph and the moderationsettings currently only being available through its beta endpoint, which will be used when -IncludeModerationSettings is provided. + ## EXAMPLES ### EXAMPLE 1 @@ -51,6 +53,13 @@ Get-PnPTeamsChannel -Team a6c1e0d7-f579-4993-81ab-4b666f8edea8 -Identity "19:796 Retrieves the channel specified by its channel id +### EXAMPLE 4 +```powershell +Get-PnPTeamsChannel -Team a6c1e0d7-f579-4993-81ab-4b666f8edea8 -Identity "19:796d063b63e34497aeaf092c8fb9b44e@thread.skype" -IncludeModerationSettings +``` + +Retrieves the channel specified by its channel id which will include the ModerationSettings + ## PARAMETERS ### -Identity @@ -81,7 +90,20 @@ Accept pipeline input: True (ByValue) Accept wildcard characters: False ``` -## RELATED LINKS +### -IncludeModerationSettings +When provided, it will use the beta endpoint of Microsoft Graph to retrieve the information. This will include the ModerationSettings if used in combination with -Identity . + +```yaml +Type: SwitchParameter +Parameter Sets: (All) -[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +## RELATED LINKS +[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) \ No newline at end of file diff --git a/documentation/Set-PnPTeamsChannel.md b/documentation/Set-PnPTeamsChannel.md index 9783075e9..5a704ba74 100644 --- a/documentation/Set-PnPTeamsChannel.md +++ b/documentation/Set-PnPTeamsChannel.md @@ -20,7 +20,7 @@ Updates an existing Teams Channel ## SYNTAX ```powershell -Set-PnPTeamsChannel -Team -Identity [-DisplayName ] [-Description ] [-IsFavoriteByDefault ] +Set-PnPTeamsChannel -Team -Identity [-DisplayName ] [-Description ] [-IsFavoriteByDefault ] [-AllowNewMessageFromBots ] [-AllowNewMessageFromConnectors ] [-ReplyRestriction ] [-UserNewMessageRestriction ] ``` @@ -46,6 +46,62 @@ Updates the channel called 'MyChannel' to make it visible to members. ## PARAMETERS +### -AllowNewMessageFromBots +Allows configuring if bots are allowed to post messages in the channel + +```yaml +Type: Boolean +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -AllowNewMessageFromConnectors +Allows configuring if connectors are allowed to post messages in the channel + +```yaml +Type: Boolean +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ReplyRestriction +Allows configuring who can reply to posts in the channel + +```yaml +Type: TeamChannelModerationSettingReplyRestriction +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -UserNewMessageRestriction +Allows configuring who can post new messages in the channel + +```yaml +Type: TeamChannelModerationSettingNewMessageRestriction +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -Description Changes the description of the specified channel. @@ -118,5 +174,4 @@ Accept wildcard characters: False ## RELATED LINKS -[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - +[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) \ No newline at end of file diff --git a/src/Commands/Base/PipeBinds/TeamsChannelPipeBind.cs b/src/Commands/Base/PipeBinds/TeamsChannelPipeBind.cs index 464691f06..e5944a526 100644 --- a/src/Commands/Base/PipeBinds/TeamsChannelPipeBind.cs +++ b/src/Commands/Base/PipeBinds/TeamsChannelPipeBind.cs @@ -49,21 +49,22 @@ public string GetId(PnPConnection connection, string accessToken, string groupId } } - public TeamChannel GetChannel(PnPConnection connection, string accessToken, string groupId) + public TeamChannel GetChannel(PnPConnection connection, string accessToken, string groupId, bool useBeta = false) { - var channels = TeamsUtility.GetChannelsAsync(accessToken, connection, groupId).GetAwaiter().GetResult(); - if(channels != null && channels.Any()) + if (!string.IsNullOrEmpty(_id)) { - if(!string.IsNullOrEmpty(_id)) - { - return channels.FirstOrDefault(c => c.Id.Equals(_id, StringComparison.OrdinalIgnoreCase)); - } else + var channel = TeamsUtility.GetChannelAsync(accessToken, connection, groupId, _id, useBeta).GetAwaiter().GetResult(); + return channel; + } + else + { + var channels = TeamsUtility.GetChannelsAsync(accessToken, connection, groupId, useBeta).GetAwaiter().GetResult(); + if (channels != null && channels.Any()) { return channels.FirstOrDefault(c => c.DisplayName.Equals(_displayName, StringComparison.OrdinalIgnoreCase)); } + return null; } - return null; } - } } diff --git a/src/Commands/Enums/TeamChannelModerationSettingNewMessageRestriction.cs b/src/Commands/Enums/TeamChannelModerationSettingNewMessageRestriction.cs new file mode 100644 index 000000000..28c7edc39 --- /dev/null +++ b/src/Commands/Enums/TeamChannelModerationSettingNewMessageRestriction.cs @@ -0,0 +1,24 @@ +namespace PnP.PowerShell.Commands.Enums +{ + /// + /// All allowed options for Team Channel post new message restrictions within the moderation settings of a Microsoft Teams team + /// + /// Documentation: https://learn.microsoft.com/graph/api/resources/channelmoderationsettings#properties + public enum TeamChannelModerationSettingNewMessageRestriction + { + /// + /// Everyone can create new posts in the channel + /// + Everyone, + + /// + /// Everyone except guests can create new posts in the channel + /// + EveryoneExceptGuests, + + /// + /// Moderators can create new posts in the channel + /// + Moderators + } +} diff --git a/src/Commands/Enums/TeamChannelModerationSettingReplyRestriction.cs b/src/Commands/Enums/TeamChannelModerationSettingReplyRestriction.cs new file mode 100644 index 000000000..e93e043e6 --- /dev/null +++ b/src/Commands/Enums/TeamChannelModerationSettingReplyRestriction.cs @@ -0,0 +1,19 @@ +namespace PnP.PowerShell.Commands.Enums +{ + /// + /// All allowed options for Team Channel reply restrictions within the moderation settings of a Microsoft Teams team + /// + /// Documentation: https://learn.microsoft.com/graph/api/resources/channelmoderationsettings#properties + public enum TeamChannelModerationSettingReplyRestriction + { + /// + /// Everyone can reply in the channel + /// + Everyone, + + /// + /// Only authors and moderators can reply in the channel + /// + AuthorAndModerators + } +} diff --git a/src/Commands/Microsoft365Groups/NewMicrosoft365Group.cs b/src/Commands/Microsoft365Groups/NewMicrosoft365Group.cs index e056ca91a..fa328a13c 100644 --- a/src/Commands/Microsoft365Groups/NewMicrosoft365Group.cs +++ b/src/Commands/Microsoft365Groups/NewMicrosoft365Group.cs @@ -122,7 +122,7 @@ protected override void ExecuteCmdlet() throw new PSArgumentException("File specified for logo does not exist."); } } - var newGroup = new Microsoft365Group() + var newGroup = new Microsoft365Group { DisplayName = DisplayName, Description = Description, diff --git a/src/Commands/Model/Teams/ChannelModerationSettings.cs b/src/Commands/Model/Teams/ChannelModerationSettings.cs new file mode 100644 index 000000000..dcfff720f --- /dev/null +++ b/src/Commands/Model/Teams/ChannelModerationSettings.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace PnP.PowerShell.Commands.Model.Teams +{ + /// + /// Moderation settings on a Teams Channel + /// + /// Documentation at https://learn.microsoft.com/graph/api/resources/channelmoderationsettings + public partial class ChannelModerationSettings + { + #region Public Members + + /// + /// Indicates whether bots are allowed to post messages + /// + [JsonPropertyName("allowNewMessageFromBots")] + public bool? AllowNewMessageFromBots { get; set; } + + /// + /// Indicates whether connectors are allowed to post messages + /// + [JsonPropertyName("allowNewMessageFromConnectors")] + public bool? AllowNewMessageFromConnectors { get; set; } + + /// + /// Indicates who is allowed to reply to the teams channel + /// + [JsonPropertyName("replyRestriction")] + public Enums.TeamChannelModerationSettingReplyRestriction? ReplyRestriction { get; set; } + + /// + /// Indicates who is allowed to post messages to teams channel + /// + [JsonPropertyName("userNewMessageRestriction")] + public Enums.TeamChannelModerationSettingNewMessageRestriction? UserNewMessageRestriction { get; set; } + + #endregion + } +} diff --git a/src/Commands/Model/Teams/Team.cs b/src/Commands/Model/Teams/Team.cs index 22808a23c..c70278681 100644 --- a/src/Commands/Model/Teams/Team.cs +++ b/src/Commands/Model/Teams/Team.cs @@ -1,5 +1,4 @@ using PnP.PowerShell.Commands.Model.Graph; -using PnP.PowerShell.Commands.Utilities.JSON; using System; using System.Collections.Generic; using System.Text.Json.Serialization; @@ -12,6 +11,7 @@ namespace PnP.PowerShell.Commands.Model.Teams public class Team { #region Public Members + public string DisplayName { get; set; } public string Classification { get; set; } diff --git a/src/Commands/Model/Teams/TeamChannel.cs b/src/Commands/Model/Teams/TeamChannel.cs index 6fe57dd1f..bb22f8187 100644 --- a/src/Commands/Model/Teams/TeamChannel.cs +++ b/src/Commands/Model/Teams/TeamChannel.cs @@ -59,8 +59,16 @@ public partial class TeamChannel /// public string Id { get; set; } + /// + /// Members of a channel + /// public List Members { get; set; } + /// + /// Settings for moderating posts in a Teams Channel + /// + public ChannelModerationSettings ModerationSettings { get; set; } = new(); + #endregion } } diff --git a/src/Commands/Model/Teams/TeamGuestSettings.cs b/src/Commands/Model/Teams/TeamGuestSettings.cs index 73b15b59c..b48396f09 100644 --- a/src/Commands/Model/Teams/TeamGuestSettings.cs +++ b/src/Commands/Model/Teams/TeamGuestSettings.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace PnP.PowerShell.Commands.Model.Teams +namespace PnP.PowerShell.Commands.Model.Teams { public partial class TeamGuestSettings { diff --git a/src/Commands/Teams/GetTeamsChannel.cs b/src/Commands/Teams/GetTeamsChannel.cs index d828c7d4c..c9f6de2b6 100644 --- a/src/Commands/Teams/GetTeamsChannel.cs +++ b/src/Commands/Teams/GetTeamsChannel.cs @@ -17,22 +17,20 @@ public class GetTeamsChannel : PnPGraphCmdlet [Parameter(Mandatory = false)] public TeamsChannelPipeBind Identity; + [Parameter(Mandatory = false)] + public SwitchParameter IncludeModerationSettings; + protected override void ExecuteCmdlet() { - var groupId = Team.GetGroupId(Connection, AccessToken); - if (groupId != null) + var groupId = Team.GetGroupId(Connection, AccessToken) ?? throw new PSArgumentException("Team not found", nameof(Team)); + + if (ParameterSpecified(nameof(Identity))) { - if (ParameterSpecified(nameof(Identity))) - { - WriteObject(Identity.GetChannel(Connection, AccessToken, groupId)); - } - else - { - WriteObject(TeamsUtility.GetChannelsAsync(AccessToken, Connection, groupId).GetAwaiter().GetResult(), true); - } - } else + WriteObject(Identity.GetChannel(Connection, AccessToken, groupId, useBeta: IncludeModerationSettings.ToBool())); + } + else { - throw new PSArgumentException("Team not found", nameof(Team)); + WriteObject(TeamsUtility.GetChannelsAsync(AccessToken, Connection, groupId, useBeta: IncludeModerationSettings.ToBool()).GetAwaiter().GetResult(), true); } } } diff --git a/src/Commands/Teams/SetTeamsChannel.cs b/src/Commands/Teams/SetTeamsChannel.cs index df41be7b2..6a834e6cf 100644 --- a/src/Commands/Teams/SetTeamsChannel.cs +++ b/src/Commands/Teams/SetTeamsChannel.cs @@ -26,65 +26,96 @@ public class SetTeamsChannel : PnPGraphCmdlet [Parameter(Mandatory = false)] public bool IsFavoriteByDefault; + [Parameter(Mandatory = false)] + public bool AllowNewMessageFromBots; + + [Parameter(Mandatory = false)] + public bool AllowNewMessageFromConnectors; + + [Parameter(Mandatory = false)] + [ArgumentCompleter(typeof(EnumAsStringArgumentCompleter))] + public Enums.TeamChannelModerationSettingReplyRestriction ReplyRestriction; + + [Parameter(Mandatory = false)] + [ArgumentCompleter(typeof(EnumAsStringArgumentCompleter))] + public Enums.TeamChannelModerationSettingNewMessageRestriction UserNewMessageRestriction; + protected override void ExecuteCmdlet() { - var groupId = Team.GetGroupId(Connection, AccessToken); - if (groupId != null) + var groupId = Team.GetGroupId(Connection, AccessToken) ?? throw new PSArgumentException("Group not found"); + var teamChannel = Identity.GetChannel(Connection, AccessToken, groupId) ?? throw new PSArgumentException("Channel not found"); + + // Flag to indicate if we have to use the beta endpoint to perform the update + var betaRequired = false; + + if (ParameterSpecified(nameof(DisplayName)) && teamChannel.DisplayName != DisplayName) + { + teamChannel.DisplayName = DisplayName; + } + else + { + teamChannel.DisplayName = null; + } + + if (ParameterSpecified(nameof(Description)) && teamChannel.Description != Description) + { + teamChannel.Description = Description; + } + else + { + teamChannel.Description = null; + } + + if (teamChannel.MembershipType.ToLower() == "standard" && ParameterSpecified(nameof(IsFavoriteByDefault)) && teamChannel.IsFavoriteByDefault != IsFavoriteByDefault) + { + teamChannel.IsFavoriteByDefault = IsFavoriteByDefault; + } + else + { + teamChannel.IsFavoriteByDefault = null; + } + + if (ParameterSpecified(nameof(AllowNewMessageFromBots))) + { + teamChannel.ModerationSettings.AllowNewMessageFromBots = AllowNewMessageFromBots; + betaRequired = true; + } + + if (ParameterSpecified(nameof(AllowNewMessageFromConnectors))) + { + teamChannel.ModerationSettings.AllowNewMessageFromConnectors = AllowNewMessageFromConnectors; + betaRequired = true; + } + + if (ParameterSpecified(nameof(ReplyRestriction))) { - var teamChannel = Identity.GetChannel(Connection, AccessToken, groupId); - if (teamChannel != null) + teamChannel.ModerationSettings.ReplyRestriction = ReplyRestriction; + betaRequired = true; + } + + if (ParameterSpecified(nameof(UserNewMessageRestriction))) + { + teamChannel.ModerationSettings.UserNewMessageRestriction = UserNewMessageRestriction; + betaRequired = true; + } + + teamChannel.MembershipType = null; + try + { + var updated = TeamsUtility.UpdateChannelAsync(Connection, AccessToken, groupId, teamChannel.Id, teamChannel, useBeta: betaRequired).GetAwaiter().GetResult(); + WriteObject(updated); + } + catch (GraphException ex) + { + if (ex.Error != null) { - if (ParameterSpecified(nameof(DisplayName)) && teamChannel.DisplayName != DisplayName) - { - teamChannel.DisplayName = DisplayName; - } else - { - teamChannel.DisplayName = null; - } - if (ParameterSpecified(nameof(Description)) && teamChannel.Description != Description) - { - teamChannel.Description = Description; - } else - { - teamChannel.Description = null; - } - - if (teamChannel.MembershipType.ToLower() == "standard" && ParameterSpecified(nameof(IsFavoriteByDefault)) && teamChannel.IsFavoriteByDefault != IsFavoriteByDefault) - { - teamChannel.IsFavoriteByDefault = IsFavoriteByDefault; - } - else - { - teamChannel.IsFavoriteByDefault = null; - } - teamChannel.MembershipType = null; - try - { - var updated = TeamsUtility.UpdateChannelAsync(Connection, AccessToken, groupId, teamChannel.Id, teamChannel).GetAwaiter().GetResult(); - WriteObject(updated); - } - catch (GraphException ex) - { - if (ex.Error != null) - { - throw new PSInvalidOperationException(ex.Error.Message); - } - else - { - throw; - } - } + throw new PSInvalidOperationException(ex.Error.Message); } else { - throw new PSArgumentException("Channel not found"); + throw; } } - else - { - throw new PSArgumentException("Group not found"); - } - } } } \ No newline at end of file diff --git a/src/Commands/Utilities/TeamsUtility.cs b/src/Commands/Utilities/TeamsUtility.cs index 6243d8a09..a5cd0d376 100644 --- a/src/Commands/Utilities/TeamsUtility.cs +++ b/src/Commands/Utilities/TeamsUtility.cs @@ -627,19 +627,28 @@ public static async Task UpdateTeamUserRole(PnPConnection connection, #endregion #region Channel - public static async Task> GetChannelsAsync(string accessToken, PnPConnection connection, string groupId) + + public static async Task GetChannelAsync(string accessToken, PnPConnection connection, string groupId, string channelId, bool useBeta = false) + { + var channel = await GraphHelper.GetAsync(connection, $"{(useBeta ? "beta" : "v1.0")}/teams/{groupId}/channels/{channelId}", accessToken); + return channel; + } + + public static async Task> GetChannelsAsync(string accessToken, PnPConnection connection, string groupId, bool useBeta = false) { - var collection = await GraphHelper.GetResultCollectionAsync(connection, $"v1.0/teams/{groupId}/channels", accessToken); + var collection = await GraphHelper.GetResultCollectionAsync(connection, $"{(useBeta ? "beta" : "v1.0")}/teams/{groupId}/channels", accessToken); return collection; } - public static async Task GetPrimaryChannelAsync(string accessToken, PnPConnection connection, string groupId) + + public static async Task GetPrimaryChannelAsync(string accessToken, PnPConnection connection, string groupId, bool useBeta = false) { - var collection = await GraphHelper.GetAsync(connection, $"v1.0/teams/{groupId}/primaryChannel", accessToken); + var collection = await GraphHelper.GetAsync(connection, $"{(useBeta ? "beta" : "v1.0")}/teams/{groupId}/primaryChannel", accessToken); return collection; } - public static async Task DeleteChannelAsync(string accessToken, PnPConnection connection, string groupId, string channelId) + + public static async Task DeleteChannelAsync(string accessToken, PnPConnection connection, string groupId, string channelId, bool useBeta = false) { - return await GraphHelper.DeleteAsync(connection, $"v1.0/teams/{groupId}/channels/{channelId}", accessToken); + return await GraphHelper.DeleteAsync(connection, $"{(useBeta ? "beta" : "v1.0")}/teams/{groupId}/channels/{channelId}", accessToken); } public static async Task AddChannelAsync(string accessToken, PnPConnection connection, string groupId, string displayName, string description, TeamsChannelType channelType, string ownerUPN, bool isFavoriteByDefault) @@ -716,9 +725,12 @@ public static async Task GetMessageReplyAsync(PnPConnec return await GraphHelper.GetAsync(connection, $"v1.0/teams/{groupId}/channels/{channelId}/messages/{messageId}/replies/{replyId}", accessToken); } - public static async Task UpdateChannelAsync(PnPConnection connection, string accessToken, string groupId, string channelId, TeamChannel channel) + /// + /// Updates a Teams Channel + /// + public static async Task UpdateChannelAsync(PnPConnection connection, string accessToken, string groupId, string channelId, TeamChannel channel, bool useBeta = false) { - return await GraphHelper.PatchAsync(connection, accessToken, $"v1.0/teams/{groupId}/channels/{channelId}", channel); + return await GraphHelper.PatchAsync(connection, accessToken, $"{(useBeta ? "beta" : "v1.0")}/teams/{groupId}/channels/{channelId}", channel); } #endregion