diff --git a/PoshBot/Classes/Bot.ps1 b/PoshBot/Classes/Bot.ps1 index 1da1bf18..5f6b7fdb 100644 --- a/PoshBot/Classes/Bot.ps1 +++ b/PoshBot/Classes/Bot.ps1 @@ -62,9 +62,9 @@ class Bot : BaseLogger { # Disable $PSStyle output rendering so ANSI escape sequences # don't get sent back to the chat backend - if ($global:PSStyle) { - $global:PSStyle.OutputRendering = [Management.Automation.OutputRendering]::Host - } + # if ($global:PSStyle) { + # $global:PSStyle.OutputRendering = [Management.Automation.OutputRendering]::Host + # } # Attach the logger to the backend $this.Backend.Logger = $this.Logger diff --git a/PoshBot/Classes/ConnectionConfig.ps1 b/PoshBot/Classes/ConnectionConfig.ps1 index 8f118208..5c9bfcf0 100644 --- a/PoshBot/Classes/ConnectionConfig.ps1 +++ b/PoshBot/Classes/ConnectionConfig.ps1 @@ -7,6 +7,8 @@ class ConnectionConfig { [pscredential]$Credential + [pscredential]$CredentialApp + ConnectionConfig() {} ConnectionConfig([string]$Endpoint, [pscredential]$Credential) { diff --git a/PoshBot/Implementations/Slack/SlackBackend.ps1 b/PoshBot/Implementations/Slack/SlackBackend.ps1 index 5e9df7fb..f7985ba3 100644 --- a/PoshBot/Implementations/Slack/SlackBackend.ps1 +++ b/PoshBot/Implementations/Slack/SlackBackend.ps1 @@ -368,6 +368,11 @@ class SlackBackend : Backend { $sendTo = "@$($this.UserIdToUsername($Response.MessageFrom))" } + $threadId = [string]::Empty + if ($customResponse.TH) { + $threadId = "@$($Response.OriginalMessage.RawMessage.ts)" + } + switch -Regex ($customResponse.PSObject.TypeNames[0]) { '(.*?)PoshBot\.Card\.Response' { $this.LogDebug('Custom response is [PoshBot.Card.Response]') @@ -420,7 +425,11 @@ class SlackBackend : Backend { $attParams.Text = [string]::Empty } $att = New-SlackMessageAttachment @attParams - $msg = $att | New-SlackMessage -Channel $sendTo -AsUser + if([string]::IsNullOrEmpty($threadId)){ + $msg = $att | New-SlackMessage -Channel $sendTo -AsUser + }else{ + $msg = $att | New-SlackMessage -Channel $sendTo -AsUser -Thread $threadId + } $this.LogDebug("Sending card response back to Slack channel [$sendTo]", $att) $msg | Send-SlackMessage -Token $this.Connection.Config.Credential.GetNetworkCredential().Password -Verbose:$false > $null } @@ -436,7 +445,11 @@ class SlackBackend : Backend { $t = $chunk } $this.LogDebug("Sending text response back to Slack channel [$sendTo]", $t) - Send-SlackMessage -Token $this.Connection.Config.Credential.GetNetworkCredential().Password -Channel $sendTo -Text $t -Verbose:$false -AsUser > $null + if([string]::IsNullOrEmpty($threadId)){ + Send-SlackMessage -Token $this.Connection.Config.Credential.GetNetworkCredential().Password -Channel $sendTo -Text $t -Verbose:$false -AsUser > $null + }else{ + Send-SlackMessage -Token $this.Connection.Config.Credential.GetNetworkCredential().Password -Channel $sendTo -Text $t -Verbose:$false -AsUser -Thread $threadId > $null + } } break } diff --git a/PoshBot/Implementations/SlackAppSM/SlackAppSMBackend.ps1 b/PoshBot/Implementations/SlackAppSM/SlackAppSMBackend.ps1 new file mode 100644 index 00000000..903e701a --- /dev/null +++ b/PoshBot/Implementations/SlackAppSM/SlackAppSMBackend.ps1 @@ -0,0 +1,909 @@ + +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Scope='Class', Target='*')] +class SlackAppSMBackend : Backend { + + # The types of message that we care about from Slack + # All othere will be ignored + [string[]]$MessageTypes = @( + 'channel_rename' + 'member_joined_channel' + 'member_left_channel' + 'message' + 'pin_added' + 'pin_removed' + 'presence_change' + 'reaction_added' + 'reaction_removed' + 'star_added' + 'star_removed' + 'goodbye' + ) + + [int]$MaxMessageLength = 3900 + + # Import some color defs. + hidden [hashtable]$_PSSlackColorMap = @{ + aliceblue = "#F0F8FF" + antiquewhite = "#FAEBD7" + aqua = "#00FFFF" + aquamarine = "#7FFFD4" + azure = "#F0FFFF" + beige = "#F5F5DC" + bisque = "#FFE4C4" + black = "#000000" + blanchedalmond = "#FFEBCD" + blue = "#0000FF" + blueviolet = "#8A2BE2" + brown = "#A52A2A" + burlywood = "#DEB887" + cadetblue = "#5F9EA0" + chartreuse = "#7FFF00" + chocolate = "#D2691E" + coral = "#FF7F50" + cornflowerblue = "#6495ED" + cornsilk = "#FFF8DC" + crimson = "#DC143C" + darkblue = "#00008B" + darkcyan = "#008B8B" + darkgoldenrod = "#B8860B" + darkgray = "#A9A9A9" + darkgreen = "#006400" + darkkhaki = "#BDB76B" + darkmagenta = "#8B008B" + darkolivegreen = "#556B2F" + darkorange = "#FF8C00" + darkorchid = "#9932CC" + darkred = "#8B0000" + darksalmon = "#E9967A" + darkseagreen = "#8FBC8F" + darkslateblue = "#483D8B" + darkslategray = "#2F4F4F" + darkturquoise = "#00CED1" + darkviolet = "#9400D3" + deeppink = "#FF1493" + deepskyblue = "#00BFFF" + dimgray = "#696969" + dodgerblue = "#1E90FF" + firebrick = "#B22222" + floralwhite = "#FFFAF0" + forestgreen = "#228B22" + fuchsia = "#FF00FF" + gainsboro = "#DCDCDC" + ghostwhite = "#F8F8FF" + gold = "#FFD700" + goldenrod = "#DAA520" + gray = "#808080" + green = "#008000" + greenyellow = "#ADFF2F" + honeydew = "#F0FFF0" + hotpink = "#FF69B4" + indianred = "#CD5C5C" + indigo = "#4B0082" + ivory = "#FFFFF0" + khaki = "#F0E68C" + lavender = "#E6E6FA" + lavenderblush = "#FFF0F5" + lawngreen = "#7CFC00" + lemonchiffon = "#FFFACD" + lightblue = "#ADD8E6" + lightcoral = "#F08080" + lightcyan = "#E0FFFF" + lightgoldenrodyellow = "#FAFAD2" + lightgreen = "#90EE90" + lightgrey = "#D3D3D3" + lightpink = "#FFB6C1" + lightsalmon = "#FFA07A" + lightseagreen = "#20B2AA" + lightskyblue = "#87CEFA" + lightslategray = "#778899" + lightsteelblue = "#B0C4DE" + lightyellow = "#FFFFE0" + lime = "#00FF00" + limegreen = "#32CD32" + linen = "#FAF0E6" + maroon = "#800000" + mediumaquamarine = "#66CDAA" + mediumblue = "#0000CD" + mediumorchid = "#BA55D3" + mediumpurple = "#9370DB" + mediumseagreen = "#3CB371" + mediumslateblue = "#7B68EE" + mediumspringgreen = "#00FA9A" + mediumturquoise = "#48D1CC" + mediumvioletred = "#C71585" + midnightblue = "#191970" + mintcream = "#F5FFFA" + mistyrose = "#FFE4E1" + moccasin = "#FFE4B5" + navajowhite = "#FFDEAD" + navy = "#000080" + oldlace = "#FDF5E6" + olive = "#808000" + olivedrab = "#6B8E23" + orange = "#FFA500" + orangered = "#FF4500" + orchid = "#DA70D6" + palegoldenrod = "#EEE8AA" + palegreen = "#98FB98" + paleturquoise = "#AFEEEE" + palevioletred = "#DB7093" + papayawhip = "#FFEFD5" + peachpuff = "#FFDAB9" + peru = "#CD853F" + pink = "#FFC0CB" + plum = "#DDA0DD" + powderblue = "#B0E0E6" + purple = "#800080" + red = "#FF0000" + rosybrown = "#BC8F8F" + royalblue = "#4169E1" + saddlebrown = "#8B4513" + salmon = "#FA8072" + sandybrown = "#F4A460" + seagreen = "#2E8B57" + seashell = "#FFF5EE" + sienna = "#A0522D" + silver = "#C0C0C0" + skyblue = "#87CEEB" + slateblue = "#6A5ACD" + slategray = "#708090" + snow = "#FFFAFA" + springgreen = "#00FF7F" + steelblue = "#4682B4" + tan = "#D2B48C" + teal = "#008080" + thistle = "#D8BFD8" + tomato = "#FF6347" + turquoise = "#40E0D0" + violet = "#EE82EE" + wheat = "#F5DEB3" + white = "#FFFFFF" + whitesmoke = "#F5F5F5" + yellow = "#FFFF00" + yellowgreen = "#9ACD32" + } + + SlackAppSMBackend ([string]$Token,[string]$TokenApp) { + Import-Module PSSlack -Verbose:$false -ErrorAction Stop + + $config = [ConnectionConfig]::new() + + $secToken = $Token | ConvertTo-SecureString -AsPlainText -Force + $secTokenApp = $TokenApp | ConvertTo-SecureString -AsPlainText -Force + + $config.Credential = New-Object System.Management.Automation.PSCredential('asdf', $secToken) + $config.CredentialApp = New-Object System.Management.Automation.PSCredential('asdf', $secTokenApp) + + $conn = [SlackAppSMConnection]::New() + $conn.Config = $config + $this.Connection = $conn + } + + # Connect to Slack + [void]Connect() { + $this.LogInfo('Connecting to backend') + $this.LogInfo('Listening for the following message types. All others will be ignored', $this.MessageTypes) + $this.Connection.Connect() + $this.BotId = $this.GetBotIdentity() + $this.LoadUsers() + $this.LoadRooms() + } + + # Receive a message from the websocket + [Message[]]ReceiveMessage() { + $messages = New-Object -TypeName System.Collections.ArrayList + try { + foreach ($slackMessage in $this.Connection.ReadReceiveJob()) { + $this.LogDebug('Received message', (ConvertTo-Json -InputObject $slackMessage -Depth 15 -Compress)) + + # Slack will sometimes send back ephemeral messages from user [SlackBot]. Ignore these + # These are messages like notifing that a message won't be unfurled because it's already + # in the channel in the last hour. Helpful message for some, but not for us. + if ($slackMessage.subtype -eq 'bot_message') { + $this.LogDebug('SubType is [bot_message]. Ignoring') + continue + } + + # Ignore "message_replied" subtypes + # These are message Slack sends to update the client that the original message has a new reply. + # That reply is sent is another message. + # We do this because if the original message that this reply is to is a bot command, the command + # will be executed again so we....need to not do that :) + if ($slackMessage.subtype -eq 'message_replied') { + $this.LogDebug('SubType is [message_replied]. Ignoring') + continue + } + + # We only care about certain message types from Slack + if ($slackMessage.Type -in $this.MessageTypes) { + $msg = [Message]::new() + + # Set the message type and optionally the subtype + #$msg.Type = $slackMessage.type + switch ($slackMessage.type) { + 'channel_rename' { + $msg.Type = [MessageType]::ChannelRenamed + } + 'member_joined_channel' { + $msg.Type = [MessageType]::Message + $msg.SubType = [MessageSubtype]::ChannelJoined + } + 'member_left_channel' { + $msg.Type = [MessageType]::Message + $msg.SubType = [MessageSubtype]::ChannelLeft + } + 'message' { + $msg.Type = [MessageType]::Message + } + 'pin_added' { + $msg.Type = [MessageType]::PinAdded + } + 'pin_removed' { + $msg.Type = [MessageType]::PinRemoved + } + 'presence_change' { + $msg.Type = [MessageType]::PresenceChange + } + 'reaction_added' { + $msg.Type = [MessageType]::ReactionAdded + } + 'reaction_removed' { + $msg.Type = [MessageType]::ReactionRemoved + } + 'star_added' { + $msg.Type = [MessageType]::StarAdded + } + 'star_removed' { + $msg.Type = [MessageType]::StarRemoved + } + 'goodbye' { + # The 'goodbye' event means Slack wants to cease comminication with us + # and they're being nice about it. We need to reestablish the connection. + $this.LogInfo('Received [goodbye] event. Reconnecting to Slack backend...') + $this.Connection.Reconnect() + return $null + } + } + + # The channel the message occured in is sometimes + # nested in an 'item' property + if ($slackMessage.item -and ($slackMessage.item.channel)) { + $msg.To = $slackMessage.item.channel + } + + if ($slackMessage.subtype) { + switch ($slackMessage.subtype) { + 'channel_join' { + $msg.Subtype = [MessageSubtype]::ChannelJoined + } + 'channel_leave' { + $msg.Subtype = [MessageSubtype]::ChannelLeft + } + 'channel_name' { + $msg.Subtype = [MessageSubtype]::ChannelRenamed + } + 'channel_purpose' { + $msg.Subtype = [MessageSubtype]::ChannelPurposeChanged + } + 'channel_topic' { + $msg.Subtype = [MessageSubtype]::ChannelTopicChanged + } + } + } + $this.LogDebug("Message type is [$($msg.Type)`:$($msg.Subtype)]") + + $msg.RawMessage = $slackMessage + $this.LogDebug('Raw message', $slackMessage) + if ($slackMessage.text) { $msg.Text = $slackMessage.text } + if ($slackMessage.channel) { $msg.To = $slackMessage.channel } + if ($slackMessage.user) { $msg.From = $slackMessage.user } + + # Resolve From name + $msg.FromName = $this.ResolveFromName($msg) + + # Resolve channel name + $msg.ToName = $this.ResolveToName($msg) + + # Mark as DM + if ($msg.To -match '^D') { + $msg.IsDM = $true + } + + # Get time of message + $unixEpoch = [datetime]'1970-01-01' + if ($slackMessage.ts) { + $msg.Time = $unixEpoch.AddSeconds($slackMessage.ts) + } elseIf ($slackMessage.event_ts) { + $msg.Time = $unixEpoch.AddSeconds($slackMessage.event_ts) + } else { + $msg.Time = [datetime]::UtcNow + } + + # Sometimes the message is nested in a 'message' subproperty. This could be + # if the message contained a link that was unfurled. We would receive a + # 'message_changed' message and need to look in the 'message' subproperty + # to see who the message was from. Slack is weird + # https://api.slack.com/events/message/message_changed + if ($slackMessage.message) { + if ($slackMessage.message.user) { + $msg.From = $slackMessage.message.user + } + if ($slackMessage.message.text) { + $msg.Text = $slackMessage.message.text + } + } + + # Slack displays @mentions like '@devblackops' but internally in the message + # it is <@U4AM3SYI8> + # Fix that so we actually see the @username + $processed = $this._ProcessMentions($msg.Text) + $msg.Text = $processed + + # ** Important safety tip, don't cross the streams ** + # Only return messages that didn't come from the bot + # else we'd cause a feedback loop with the bot processing + # it's own responses + if (-not $this.MsgFromBot($msg.From)) { + $messages.Add($msg) > $null + } + } else { + $this.LogDebug("Message type is [$($slackMessage.Type)]. Ignoring") + } + + } + } catch { + Write-Error $_ + } + + return $messages + } + + # Send a Slack ping + [void]Ping() { + } + + # Send a message back to Slack + [void]SendMessage([Response]$Response) { + # Process any custom responses + $this.LogDebug("[$($Response.Data.Count)] custom responses") + foreach ($customResponse in $Response.Data) { + + [string]$sendTo = $Response.To + if ($customResponse.DM) { + $sendTo = "@$($this.UserIdToUsername($Response.MessageFrom))" + } + + $threadId = [string]::Empty + if ($customResponse.TH) { + $threadId = "@$($Response.OriginalMessage.RawMessage.ts)" + } + + switch -Regex ($customResponse.PSObject.TypeNames[0]) { + '(.*?)PoshBot\.Card\.Response' { + $this.LogDebug('Custom response is [PoshBot.Card.Response]') + $chunks = $this._ChunkString($customResponse.Text) + $x = 0 + foreach ($chunk in $chunks) { + $attParams = @{ + MarkdownFields = 'text' + Color = $customResponse.Color + } + $fbText = 'no data' + if (-not [string]::IsNullOrEmpty($chunk.Text)) { + $this.LogDebug("Response size [$($chunk.Text.Length)]") + $fbText = $chunk.Text + } + $attParams.Fallback = $fbText + if ($customResponse.Title) { + + # If we chunked up the response, only display the title on the first one + if ($x -eq 0) { + $attParams.Title = $customResponse.Title + } + } + if ($customResponse.ImageUrl) { + $attParams.ImageURL = $customResponse.ImageUrl + } + if ($customResponse.ThumbnailUrl) { + $attParams.ThumbURL = $customResponse.ThumbnailUrl + } + if ($customResponse.LinkUrl) { + $attParams.TitleLink = $customResponse.LinkUrl + } + if ($customResponse.Fields) { + $arr = New-Object System.Collections.ArrayList + foreach ($key in $customResponse.Fields.Keys) { + $arr.Add( + @{ + title = $key; + value = $customResponse.Fields[$key]; + short = $true + } + ) + } + $attParams.Fields = $arr + } + + if (-not [string]::IsNullOrEmpty($chunk)) { + $attParams.Text = '```' + $chunk + '```' + } else { + $attParams.Text = [string]::Empty + } + $att = New-SlackMessageAttachment @attParams + if([string]::IsNullOrEmpty($threadId)){ + $msg = $att | New-SlackMessage -Channel $sendTo -AsUser + }else{ + $msg = $att | New-SlackMessage -Channel $sendTo -AsUser -Thread $threadId + } + $this.LogDebug("Sending card response back to Slack channel [$sendTo]", $att) + $msg | Send-SlackMessage -Token $this.Connection.Config.Credential.GetNetworkCredential().Password -Verbose:$false > $null + } + break + } + '(.*?)PoshBot\.Text\.Response' { + $this.LogDebug('Custom response is [PoshBot.Text.Response]') + $chunks = $this._ChunkString($customResponse.Text) + foreach ($chunk in $chunks) { + if ($customResponse.AsCode) { + $t = '```' + $chunk + '```' + } else { + $t = $chunk + } + $this.LogDebug("Sending text response back to Slack channel [$sendTo]", $t) + if([string]::IsNullOrEmpty($threadId)){ + Send-SlackMessage -Token $this.Connection.Config.Credential.GetNetworkCredential().Password -Channel $sendTo -Text $t -Verbose:$false -AsUser > $null + }else{ + Send-SlackMessage -Token $this.Connection.Config.Credential.GetNetworkCredential().Password -Channel $sendTo -Text $t -Verbose:$false -AsUser -Thread $threadId > $null + } + } + break + } + '(.*?)PoshBot\.File\.Upload' { + $this.LogDebug('Custom response is [PoshBot.File.Upload]') + + $uploadParams = @{ + Token = $this.Connection.Config.Credential.GetNetworkCredential().Password + Channel = $sendTo + } + + if ([string]::IsNullOrEmpty($customResponse.Path) -and (-not [string]::IsNullOrEmpty($customResponse.Content))) { + $uploadParams.Content = $customResponse.Content + if (-not [string]::IsNullOrEmpty($customResponse.FileType)) { + $uploadParams.FileType = $customResponse.FileType + } + if (-not [string]::IsNullOrEmpty($customResponse.FileName)) { + $uploadParams.FileName = $customResponse.FileName + } + } else { + # Test if file exists and send error response if not found + if (-not (Test-Path -Path $customResponse.Path -ErrorAction SilentlyContinue)) { + # Mark command as failed since we could't find the file to upload + $this.RemoveReaction($Response.OriginalMessage, [ReactionType]::Success) + $this.AddReaction($Response.OriginalMessage, [ReactionType]::Failure) + $att = New-SlackMessageAttachment -Color '#FF0000' -Title 'Rut row' -Text "File [$($uploadParams.Path)] not found" -Fallback 'Rut row' + $msg = $att | New-SlackMessage -Channel $sendTo -AsUser + $this.LogDebug("Sending card response back to Slack channel [$sendTo]", $att) + $null = $msg | Send-SlackMessage -Token $this.Connection.Config.Credential.GetNetworkCredential().Password -Verbose:$false + break + } + + $this.LogDebug("Uploading [$($customResponse.Path)] to Slack channel [$sendTo]") + $uploadParams.Path = $customResponse.Path + $uploadParams.Title = Split-Path -Path $customResponse.Path -Leaf + } + + if (-not [string]::IsNullOrEmpty($customResponse.Title)) { + $uploadParams.Title = $customResponse.Title + } + + Send-SlackFile @uploadParams -Verbose:$false + if (-not $customResponse.KeepFile -and -not [string]::IsNullOrEmpty($customResponse.Path)) { + Remove-Item -LiteralPath $customResponse.Path -Force + } + break + } + } + } + + if ($Response.Text.Count -gt 0) { + foreach ($t in $Response.Text) { + $this.LogDebug("Sending response back to Slack channel [$($Response.To)]", $t) + Send-SlackMessage -Token $this.Connection.Config.Credential.GetNetworkCredential().Password -Channel $Response.To -Text $t -Verbose:$false -AsUser > $null + } + } + } + + # Add a reaction to an existing chat message + [void]AddReaction([Message]$Message, [ReactionType]$Type, [string]$Reaction) { + if ($Message.RawMessage.ts) { + if ($Type -eq [ReactionType]::Custom) { + $emoji = $Reaction + } else { + $emoji = $this._ResolveEmoji($Type) + } + + $body = @{ + name = $emoji + channel = $Message.To + timestamp = $Message.RawMessage.ts + } + $this.LogDebug("Adding reaction [$emoji] to message Id [$($Message.RawMessage.ts)]") + $resp = Send-SlackApi -Token $this.Connection.Config.Credential.GetNetworkCredential().Password -Method 'reactions.add' -Body $body -Verbose:$false + if (-not $resp.ok) { + $this.LogInfo([LogSeverity]::Error, 'Error adding reaction to message', $resp) + } + } + } + + # Remove a reaction from an existing chat message + [void]RemoveReaction([Message]$Message, [ReactionType]$Type, [string]$Reaction) { + if ($Message.RawMessage.ts) { + if ($Type -eq [ReactionType]::Custom) { + $emoji = $Reaction + } else { + $emoji = $this._ResolveEmoji($Type) + } + + $body = @{ + name = $emoji + channel = $Message.To + timestamp = $Message.RawMessage.ts + } + $this.LogDebug("Removing reaction [$emoji] from message Id [$($Message.RawMessage.ts)]") + $resp = Send-SlackApi -Token $this.Connection.Config.Credential.GetNetworkCredential().Password -Method 'reactions.remove' -Body $body -Verbose:$false + if (-not $resp.ok) { + $this.LogInfo([LogSeverity]::Error, 'Error removing reaction from message', $resp) + } + } + } + + # Resolve a channel name to an Id + [string]ResolveChannelId([string]$ChannelName) { + if ($ChannelName -match '^#') { + $ChannelName = $ChannelName.TrimStart('#') + } + $channelId = ($this.Connection.LoginData.channels | Where-Object name -eq $ChannelName).id + if (-not $ChannelId) { + $channelId = ($this.Connection.LoginData.channels | Where-Object id -eq $ChannelName).id + } + $this.LogDebug("Resolved channel [$ChannelName] to [$channelId]") + return $channelId + } + + # Populate the list of users the Slack team + [void]LoadUsers() { + $this.LogDebug('Getting Slack users') + $allUsers = Get-Slackuser -Token $this.Connection.Config.Credential.GetNetworkCredential().Password -Verbose:$false + + # $allUsers = Get-Slackuser -Token $token_bot -Verbose:$false + + $this.LogDebug("[$($allUsers.Count)] users returned") + $allUsers | ForEach-Object { + $user = [SlackPerson]::new() + $user.Id = $_.ID + $user.Nickname = $_.Name + $user.FullName = $_.RealName + $user.FirstName = $_.FirstName + $user.LastName = $_.LastName + $user.Email = $_.Email + $user.Phone = $_.Phone + $user.Skype = $_.Skype + $user.IsBot = $_.IsBot + $user.IsAdmin = $_.IsAdmin + $user.IsOwner = $_.IsOwner + $user.IsPrimaryOwner = $_.IsPrimaryOwner + $user.IsUltraRestricted = $_.IsUltraRestricted + $user.Status = $_.Status + $user.TimeZoneLabel = $_.TimeZoneLabel + $user.TimeZone = $_.TimeZone + $user.Presence = $_.Presence + $user.Deleted = $_.Deleted + if (-not $this.Users.ContainsKey($_.ID)) { + $this.LogDebug("Adding user [$($_.ID):$($_.Name)]") + $this.Users[$_.ID] = $user + } + } + + foreach ($key in $this.Users.Keys) { + if ($key -notin $allUsers.ID) { + $this.LogDebug("Removing outdated user [$key]") + $this.Users.Remove($key) + } + } + } + + # Populate the list of channels in the Slack team + [void]LoadRooms() { + $this.LogDebug('Getting Slack channels') + $getChannelParams = @{ + Token = $this.Connection.Config.Credential.GetNetworkCredential().Password + ExcludeArchived = $true + Verbose = $false + Paging = $true + } + $allChannels = Get-SlackChannel @getChannelParams + $this.LogDebug("[$($allChannels.Count)] channels returned") + + $allChannels.ForEach({ + $channel = [SlackChannel]::new() + $channel.Id = $_.ID + $channel.Name = $_.Name + $channel.Topic = $_.Topic + $channel.Purpose = $_.Purpose + $channel.Created = $_.Created + $channel.Creator = $_.Creator + $channel.IsArchived = $_.IsArchived + $channel.IsGeneral = $_.IsGeneral + $channel.MemberCount = $_.MemberCount + foreach ($member in $_.Members) { + $channel.Members.Add($member, $null) + } + $this.LogDebug("Adding channel: $($_.ID):$($_.Name)") + $this.Rooms[$_.ID] = $channel + }) + + foreach ($key in $this.Rooms.Keys) { + if ($key -notin $allChannels.ID) { + $this.LogDebug("Removing outdated channel [$key]") + $this.Rooms.Remove($key) + } + } + } + + # Get the bot identity Id + [string]GetBotIdentity() { + $id = $this.Connection.LoginData.self.id + $this.LogVerbose("Bot identity is [$id]") + return $id + } + + # Determine if incoming message was from the bot + [bool]MsgFromBot([string]$From) { + $frombot = ($this.BotId -eq $From) + if ($fromBot) { + $this.LogDebug("Message is from bot [From: $From == Bot: $($this.BotId)]. Ignoring") + } else { + $this.LogDebug("Message is not from bot [From: $From <> Bot: $($this.BotId)]") + } + return $fromBot + } + + # Get a user by their Id + [SlackPerson]GetUser([string]$UserId) { + $user = $this.Users[$UserId] + if (-not $user) { + $this.LogDebug([LogSeverity]::Warning, "User [$UserId] not found. Refreshing users") + $this.LoadUsers() + $user = $this.Users[$UserId] + } + + if ($user) { + $this.LogDebug("Resolved user [$UserId]", $user) + } else { + $this.LogDebug([LogSeverity]::Warning, "Could not resolve user [$UserId]") + } + return $user + } + + # Get a user Id by their name + [string]UsernameToUserId([string]$Username) { + $Username = $Username.TrimStart('@') + $user = $this.Users.Values | Where-Object {$_.Nickname -eq $Username} + $id = $null + if ($user) { + $id = $user.Id + } else { + # User each doesn't exist or is not in the local cache + # Refresh it and try again + $this.LogDebug([LogSeverity]::Warning, "User [$Username] not found. Refreshing users") + $this.LoadUsers() + $user = $this.Users.Values | Where-Object {$_.Nickname -eq $Username} + if (-not $user) { + $id = $null + } else { + $id = $user.Id + } + } + if ($id) { + $this.LogDebug("Resolved [$Username] to [$id]") + } else { + $this.LogDebug([LogSeverity]::Warning, "Could not resolve user [$Username]") + } + return $id + } + + # Get a user name by their Id + [string]UserIdToUsername([string]$UserId) { + $name = $null + if ($this.Users.ContainsKey($UserId)) { + $name = $this.Users[$UserId].Nickname + } else { + $this.LogDebug([LogSeverity]::Warning, "User [$UserId] not found. Refreshing users") + $this.LoadUsers() + $name = $this.Users[$UserId].Nickname + } + if ($name) { + $this.LogDebug("Resolved [$UserId] to [$name]") + } else { + $this.LogDebug([LogSeverity]::Warning, "Could not resolve user [$UserId]") + } + return $name + } + + # Get the channel name by Id + [string]ChannelIdToName([string]$ChannelId) { + $name = $null + if ($this.Rooms.ContainsKey($ChannelId)) { + $name = $this.Rooms[$ChannelId].Name + } else { + $this.LogDebug([LogSeverity]::Warning, "Channel [$ChannelId] not found. Refreshing channels") + $this.LoadRooms() + $name = $this.Rooms[$ChannelId].Name + } + if ($name) { + $this.LogDebug("Resolved [$ChannelId] to [$name]") + } else { + $this.LogDebug([LogSeverity]::Warning, "Could not resolve channel [$ChannelId]") + } + return $name + } + + # Resolve From name + [string]ResolveFromName([Message]$Message) { + $fromName = $null + if ($Message.From) { + $fromName = $this.UserIdToUsername($Message.From) + } + return $fromName + } + + # Resolve To name + [string]ResolveToName([Message]$Message) { + # Skip DM channels, they won't have names + $toName = $null + if ($Message.To -and $Message.To -notmatch '^D') { + $toName = $this.ChannelIdToName($Message.To) + } + return $toName + } + + # Get all user info by their ID + [hashtable]GetUserInfo([string]$UserId) { + $user = $null + if ($this.Users.ContainsKey($UserId)) { + $user = $this.Users[$UserId] + } else { + $this.LogDebug([LogSeverity]::Warning, "User [$UserId] not found. Refreshing users") + $this.LoadUsers() + $user = $this.Users[$UserId] + } + + if ($user) { + $this.LogDebug("Resolved [$UserId] to [$($user.Nickname)]") + return $user.ToHash() + } else { + $this.LogDebug([LogSeverity]::Warning, "Could not resolve channel [$UserId]") + return $null + } + } + + # Remove extra characters that Slack decorates urls with + hidden [string] _SanitizeURIs([string]$Text) { + $sanitizedText = $Text -replace '<([^\|>]+)\|([^\|>]+)>', '$2' + $sanitizedText = $sanitizedText -replace '<(http([^>]+))>', '$1' + return $sanitizedText + } + + # Break apart a string by number of characters + # This isn't a very efficient method but it splits the message cleanly on + # whole lines and produces better output + hidden [Collections.Generic.List[string]] _ChunkString([string]$Text) { + + # Don't bother chunking an empty string + if ([string]::IsNullOrEmpty($Text)) { + return $text + } + + $chunks = [Collections.Generic.List[string]]::new() + $currentChunkLength = 0 + $currentChunk = '' + $array = $Text -split [Environment]::NewLine + + foreach ($line in $array) { + if (($currentChunkLength + $line.Length) -lt $this.MaxMessageLength) { + $currentChunkLength += $line.Length + $currentChunk += ($line + [Environment]::NewLine) + } else { + $chunks.Add($currentChunk + [Environment]::NewLine) + $currentChunk = ($line + [Environment]::NewLine) + $currentChunkLength = $line.Length + } + } + $chunks.Add($currentChunk) + + return $chunks + } + + # Resolve a reaction type to an emoji + hidden [string]_ResolveEmoji([ReactionType]$Type) { + $emoji = [string]::Empty + Switch ($Type) { + 'Success' { return 'white_check_mark' } + 'Failure' { return 'exclamation' } + 'Processing' { return 'gear' } + 'Warning' { return 'warning' } + 'ApprovalNeeded' { return 'closed_lock_with_key'} + 'Cancelled' { return 'no_entry_sign'} + 'Denied' { return 'x'} + } + return $emoji + } + + # Translate formatted @mentions like <@U4AM3SYI8> into @devblackops + hidden [string]_ProcessMentions([string]$Text) { + $processed = $Text + + $mentions = $processed | Select-String -Pattern '(?<@[^>]*>*)' -AllMatches | ForEach-Object { + $_.Matches | ForEach-Object { + [pscustomobject]@{ + FormattedId = $_.Value + UnformattedId = $_.Value.TrimStart('<@').TrimEnd('>') + } + } + } + $mentions | ForEach-Object { + if ($name = $this.UserIdToUsername($_.UnformattedId)) { + $processed = $processed -replace $_.FormattedId, "@$name" + $this.LogDebug($processed) + } else { + $this.LogDebug([LogSeverity]::Warning, "Unable to translate @mention [$($_.FormattedId)] into a username") + } + } + + return $processed + } +} + +function New-PoshBotSlackAppSMBackend { + <# + .SYNOPSIS + Create a new instance of a Slack backend + .DESCRIPTION + Create a new instance of a Slack backend + .PARAMETER Configuration + The hashtable containing backend-specific properties on how to create the Slack backend instance. + .EXAMPLE + PS C:\> $backendConfig = @{Name = 'SlackBackend'; Token = ''} + PS C:\> $backend = New-PoshBotSlackBackend -Configuration $backendConfig + + Create a Slack backend using the specified API token + .INPUTS + Hashtable + .OUTPUTS + SlackBackend + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Scope='Function', Target='*')] + [cmdletbinding()] + param( + [parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [Alias('BackendConfiguration')] + [hashtable[]]$Configuration + ) + + process { + foreach ($item in $Configuration) { + if (-not $item.Token) { + throw 'Configuration is missing [Token] parameter' + } else { + Write-Verbose 'Creating new Slack backend instance' + $backend = [SlackAppSMBackend]::new($item.Token,$item.TokenApp) + if ($item.Name) { + $backend.Name = $item.Name + } + $backend + } + } + } +} + +Export-ModuleMember -Function 'New-PoshBotSlackAppSMBackend' diff --git a/PoshBot/Implementations/SlackAppSM/SlackAppSMConnection.ps1 b/PoshBot/Implementations/SlackAppSM/SlackAppSMConnection.ps1 new file mode 100644 index 00000000..8e607459 --- /dev/null +++ b/PoshBot/Implementations/SlackAppSM/SlackAppSMConnection.ps1 @@ -0,0 +1,284 @@ +class SlackAppSMConnection : Connection { + [pscustomobject]$LoginData + [string]$UserName + [string]$Domain + [string]$WebSocketUrl + [bool]$Connected + [object]$ReceiveJob = $null + + [void]Connect() { + if ($null -eq $this.ReceiveJob -or $this.ReceiveJob.State -ne 'Running') { + $this.LogDebug('Connecting to Slack Real Time API') + $this.RtmConnect() + $this.StartReceiveJob() + } else { + $this.LogDebug([LogSeverity]::Warning, 'Receive job is already running') + } + } + + # Log in to Slack with the bot token and get a URL to connect to via websockets + [void]RtmConnect() { + $token_app = $this.Config.CredentialApp.GetNetworkCredential().Password # xapp-... + $token_bot = $this.Config.Credential.GetNetworkCredential().Password # xapp-... + # $url = "https://slack.com/api/rtm.connect?token=$($token)&pretty=1" + + # https://api.slack.com/apis/socket-mode + $url = 'https://slack.com/api/apps.connections.open' + + try { + + $r = Invoke-RestMethod -Method POST -Uri $url -Verbose:$false -Headers @{ + 'Content-type' = 'application/x-www-form-urlencoded' + Authorization = "Bearer $token_app" + } + + $rb = Invoke-RestMethod -Method POST -Uri 'https://slack.com/api/auth.test?pretty=1' -Verbose:$false -Headers @{ + 'Content-type' = 'application/x-www-form-urlencoded' + Authorization = "Bearer $token_bot" + } + + $rc = Invoke-RestMethod -Method GET -Uri 'https://slack.com/api/conversations.list' -Headers @{ + 'Content-type' = 'application/x-www-form-urlencoded' + Authorization = "Bearer $token_bot" + } + #$rc.channels + # LoginData.self.id + # $this.Connection.LoginData.channels + + # $r = Invoke-RestMethod -Uri $url -Method Get -Verbose:$false + # $this.LoginData = $r + $this.LoginData = [PsCustomObject]@{ + channels = $rc.channels + self = [PsCustomObject]@{ + id = $rb.user_id + } + } + + if ($r.ok) { + $this.LogInfo('Successfully authenticated to Slack Real Time API') + $this.WebSocketUrl = $r.url + $this.Domain = $rb.team #$rb.team.domain + $this.UserName = $rb.user #$r.self.name + } else { + throw $r + } + } catch { + $this.LogInfo([LogSeverity]::Error, 'Error connecting to Slack Real Time API', [ExceptionFormatter]::Summarize($_)) + } + } + + # Setup the websocket receive job + [void]StartReceiveJob() { + $recv = { + [cmdletbinding()] + param( + [parameter(mandatory)] + $url + ) + + # To keep track of ping messages + $pingIntervalSeconds = 10 + $lastMsgId = 0 + + $InformationPreference = 'Continue' + $VerbosePreference = 'Continue' + $DebugPreference = 'Continue' + $ErrorActionPreference = 'Continue' + + # Timer for sending pings + $stopWatch = [Diagnostics.Stopwatch]::new() + $stopWatch.Start() + + # Remove extra characters that Slack decorates urls with + function SanitizeURIs { + param( + [Parameter(mandatory)] + [string]$Text + ) + + $sanitizedText = $Text -replace '<([^\|>]+)\|([^\|>]+)>', '$2' + $sanitizedText = $sanitizedText -replace '<(http([^>]+))>', '$1' + $sanitizedText + } + + # Messages sent via RTM should have a unique, incrementing ID + # https://api.slack.com/rtm + function Get-NextMsgId { + $script:lastMsgId += 1 + $script:lastMsgId + } + + function Send-Ping() { + $json = @{ + id = Get-NextMsgId + type = 'ping' + } | ConvertTo-Json + + [ArraySegment[byte]]$bytes = [Text.Encoding]::UTF8.GetBytes($json) + $webSocket.SendAsync($bytes, [Net.WebSockets.WebSocketMessageType]::Text, $true, $ct).GetAwaiter().GetResult() > $null + } + + # Slack enforces TLS12 on the websocket API + [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + + # Connect to websocket + $redactedUrl = "$(Split-Path $url -Parent)\REDACTED" + Write-Verbose "Connecting to websocket at [$($redactedUrl)]" + if ($PSVersionTable.PSVersion.Major -lt 6) { + # In PowerShell 5.1 (and probably 5.0), there is a bug where the websocket conenction will disconenct after 100 seconds + # The workaround is to change the keepalive internval to 0 OR adjust the max service point idel time + # https://stackoverflow.com/questions/40502921/net-websockets-forcibly-closed-despite-keep-alive-and-activity-on-the-connectio + Write-Verbose "PowerShell version is [$($PSVersionTable.PSVersion.ToString())]. Setting [System.Net.ServicePointManager]::MaxServicePointIdleTime to [$([int]::MaxValue.ToString())] to avoid disconnects at 100 seconds." + [System.Net.ServicePointManager]::MaxServicePointIdleTime = [Int]::MaxValue + } + $webSocket = [Net.WebSockets.ClientWebSocket]::new() + $webSocket.Options.KeepAliveInterval = 5 + $cts = [Threading.CancellationTokenSource]::new() + $task = $webSocket.ConnectAsync($url, $cts.Token) + do { [Threading.Thread]::Sleep(10) } + until ($task.IsCompleted) + + # Receive messages and put on output stream so the backend can read them + $buffer = [Net.WebSockets.WebSocket]::CreateClientBuffer(1024,1024) + $ct = [Threading.CancellationToken]::new($false) + $taskResult = $null + + Write-Verbose 'Beginning websocker receive loop' + while ($webSocket.State -eq [Net.WebSockets.WebSocketState]::Open) { + $jsonResult = '' + do { + $taskResult = $webSocket.ReceiveAsync($buffer, $ct) + while (-not $taskResult.IsCompleted -and $webSocket.State -eq [Net.WebSockets.WebSocketState]::Open) { + [Threading.Thread]::Sleep(10) + + # Send "ping" every 5 seconds + if ($stopWatch.Elapsed.Seconds -ge $pingIntervalSeconds) { + Send-Ping + $stopWatch.Restart() + } + } + + if ($webSocket.State -ne [Net.WebSockets.WebSocketState]::Open) { + Write-Error "Websocket error. Connection state is [$($webSocket.State)]" + } + + $jsonResult += [Text.Encoding]::UTF8.GetString($buffer, 0, $taskResult.Result.Count) + } until ( + $webSocket.State -ne [Net.WebSockets.WebSocketState]::Open -or $taskResult.Result.EndOfMessage + ) + + + + if (-not [string]::IsNullOrEmpty($jsonResult)) { + + #region acknowledgment + # Prepare the acknowledgment message + $ackPayload = @{ + envelope_id = ($jsonResult | convertfrom-json).envelope_id + } | ConvertTo-Json + + # Convert the message to bytes + $ackBytes = [System.Text.Encoding]::UTF8.GetBytes($ackPayload) + + # Send the acknowledgment + $sendBuffer = [ArraySegment[byte]]$ackBytes + # Send - v1 + # $sendTask = $webSocket.SendAsync($sendBuffer, [Net.WebSockets.WebSocketMessageType]::Text, $true, $ct) + # $sendTask.Wait() + + # Send - v2 + $webSocket.SendAsync($sendBuffer, [Net.WebSockets.WebSocketMessageType]::Text, $true, $ct).GetAwaiter().GetResult() > $null + + #endregion acknowledgment + + # Write-Debug "Received JSON: $jsonResult" + $sanitizedJson = SanitizeURIs -Text $jsonResult + + $msgs = ConvertFrom-Json $sanitizedJson | Select-Object -ExpandProperty payload | Select-Object -ExpandProperty event + foreach ($msg in $msgs) { + # Ingore "pong" and "hello" messages as they aren't important to the backend + if ($msg.type -ne 'pong' -and $msg.type -ne 'hello') { + $msg + } + } + } + } + + $socketStatus = [pscustomobject]@{ + State = $webSocket.State + CloseStatus = $webSocket.CloseStatus + CloseStatusDescription = $webSocket.CloseStatusDescription + } + $socketStatusStr = ($socketStatus | Format-List | Out-String).Trim() + Write-Warning -Message "Websocket state is [$($webSocket.State.ToString())].`n$socketStatusStr" + } + + try { + $jobParams = @{ + Name = 'ReceiveRtmMessages' + ScriptBlock = $recv + ArgumentList = $this.WebSocketUrl + } + $this.ReceiveJob = Start-Job @jobParams + $this.Connected = $true + $this.Status = [ConnectionStatus]::Connected + $this.LogInfo("Started websocket receive job [$($this.ReceiveJob.Id)]") + } catch { + $this.LogInfo([LogSeverity]::Error, "$($_.Exception.Message)", [ExceptionFormatter]::Summarize($_)) + } + } + + # Read all available data from the job + [System.Collections.Generic.List[PSCustomObject]]ReadReceiveJob() { + # Read stream info from the job so we can log them + $infoStream = $this.ReceiveJob.ChildJobs[0].Information.ReadAll() + $warningStream = $this.ReceiveJob.ChildJobs[0].Warning.ReadAll() + $errStream = $this.ReceiveJob.ChildJobs[0].Error.ReadAll() + $verboseStream = $this.ReceiveJob.ChildJobs[0].Verbose.ReadAll() + $debugStream = $this.ReceiveJob.ChildJobs[0].Debug.ReadAll() + foreach ($item in $infoStream) { + $this.LogInfo($item.ToString()) + } + foreach ($item in $warningStream) { + $this.LogInfo([LogSeverity]::Warning, $item.ToString()) + } + foreach ($item in $errStream) { + $this.LogInfo([LogSeverity]::Error, $item.ToString()) + } + foreach ($item in $verboseStream) { + $this.LogVerbose($item.ToString()) + } + foreach ($item in $debugStream) { + $this.LogVerbose($item.ToString()) + } + + # The receive job stopped for some reason. Reestablish the connection if the job isn't running + if ($this.ReceiveJob.State -ne 'Running') { + $this.LogInfo([LogSeverity]::Warning, "Receive job state is [$($this.ReceiveJob.State)]. Attempting to reconnect...") + $this.Reconnect() + } + + $messages = [Collections.Generic.List[PSCustomObject]]::new() + if ($this.ReceiveJob.HasMoreData) { + $messages.AddRange($this.ReceiveJob.ChildJobs[0].Output.ReadAll()) + } + return $messages + } + + # Stop the receive thread + [void]Disconnect() { + $this.LogInfo('Closing websocket') + if ($this.ReceiveJob) { + $this.LogInfo("Stopping receive job [$($this.ReceiveJob.Id)]") + $this.ReceiveJob | Stop-Job -Confirm:$false -PassThru | Remove-Job -Force -ErrorAction SilentlyContinue + } + $this.Connected = $false + $this.Status = [ConnectionStatus]::Disconnected + } + + [void]Reconnect() { + $this.Disconnect() + Start-Sleep -Seconds 5 + $this.Connect() + } +} diff --git a/PoshBot/Implementations/Teams/TeamsBackend.ps1 b/PoshBot/Implementations/Teams/TeamsBackend.ps1 index 2cd01052..fdfa3ac5 100644 --- a/PoshBot/Implementations/Teams/TeamsBackend.ps1 +++ b/PoshBot/Implementations/Teams/TeamsBackend.ps1 @@ -9,12 +9,12 @@ class TeamsBackend : Backend { 'message' ) - [string]$TeamId = $null + [string]$TeamId = $null [string]$ServiceUrl = 'https://smba.trafficmanager.net/amer/' - [string]$BotId = $null - [string]$BotName = $null - [string]$TenantId = $null - [bool]$Initialized = $false + [string]$BotId = $null + [string]$BotName = $null + [string]$TenantId = $null + [bool]$Initialized = $false [hashtable]$DMConverations = @{} @@ -72,7 +72,7 @@ class TeamsBackend : Backend { $msg.RawMessage = $teamsMessage $this.LogDebug('Raw message', $teamsMessage) - if ($teamsMessage.text) { + if ($teamsMessage.text) { # When commands are directed to PoshBot, the bot must be "at" mentioned. # This will show up in the text of the message received. We don't need it so strip it out. $msg.Text = $teamsMessage.text.Replace("$($this.Connection.Config.BotName) ", '') -Replace '\n', '' @@ -82,7 +82,7 @@ class TeamsBackend : Backend { } if ($teamsMessage.from) { - $msg.From = $teamsMessage.from.id + $msg.From = $teamsMessage.from.id $msg.FromName = $teamsMessage.from.name } @@ -127,14 +127,14 @@ class TeamsBackend : Backend { # Send a message [void]SendMessage([Response]$Response) { - $baseUrl = $Response.OriginalMessage.RawMessage.serviceUrl - $fromId = $Response.OriginalMessage.RawMessage.from.id - $fromName = $Response.OriginalMessage.RawMessage.from.name - $recipientId = $Response.OriginalMessage.RawMessage.recipient.id - $recipientName = $Response.OriginalMessage.RawMessage.recipient.name + $baseUrl = $Response.OriginalMessage.RawMessage.serviceUrl + $fromId = $Response.OriginalMessage.RawMessage.from.id + $fromName = $Response.OriginalMessage.RawMessage.from.name + $recipientId = $Response.OriginalMessage.RawMessage.recipient.id + $recipientName = $Response.OriginalMessage.RawMessage.recipient.name $conversationId = $Response.OriginalMessage.RawMessage.conversation.id - $activityId = $Response.OriginalMessage.RawMessage.id - $responseUrl = "$($baseUrl)v3/conversations/$conversationId/activities/$activityId" + $activityId = $Response.OriginalMessage.RawMessage.id + $responseUrl = "$($baseUrl)v3/conversations/$conversationId/activities/$activityId" $headers = @{ Authorization = "Bearer $($this.Connection._AccessTokenInfo.access_token)" } @@ -159,26 +159,26 @@ class TeamsBackend : Backend { $this.LogDebug('Custom response is [PoshBot.Card.Response]') $cardBody = @{ - type = 'message' - from = @{ + type = 'message' + from = @{ id = $fromId name = $fromName } conversation = @{ id = $conversationId } - recipient = @{ - id = $recipientId + recipient = @{ + id = $recipientId name = $recipientName } - attachments = @( + attachments = @( @{ contentType = 'application/vnd.microsoft.teams.card.o365connector' - content = @{ - "@type" = 'MessageCard' - "@context" = 'http://schema.org/extensions' + content = @{ + '@type' = 'MessageCard' + '@context' = 'http://schema.org/extensions' themeColor = $customResponse.Color -replace '#', '' - sections = @( + sections = @( @{ } @@ -186,7 +186,7 @@ class TeamsBackend : Backend { } } ) - replyToId = $activityId + replyToId = $activityId } # Thumbnail @@ -216,7 +216,7 @@ class TeamsBackend : Backend { $cardBody.attachments[0].content.sections[0].facts = @() foreach ($field in $customResponse.Fields.GetEnumerator()) { $cardBody.attachments[0].content.sections[0].facts += @{ - name = $field.Name + name = $field.Name value = $field.Value.ToString() } } @@ -265,20 +265,20 @@ class TeamsBackend : Backend { } $cardBody = @{ - type = 'message' - from = @{ + type = 'message' + from = @{ id = $fromId name = $fromName } conversation = @{ id = $conversationId } - recipient = @{ - id = $recipientId + recipient = @{ + id = $recipientId name = $recipientName } - text = $cardText - textFormat = $textFormat + text = $cardText + textFormat = $textFormat # attachments = @( # @{ # contentType = 'application/vnd.microsoft.teams.card.o365connector' @@ -290,7 +290,7 @@ class TeamsBackend : Backend { # } # } # ) - replyToId = $activityId + replyToId = $activityId } $body = $cardBody | ConvertTo-Json -Depth 15 @@ -319,21 +319,21 @@ class TeamsBackend : Backend { # Teams doesn't support generic file uploads yet :( # Send a message informing the user of this sad fact $jsonResponse = @{ - type = 'message' - from = @{ - id = $recipientId + type = 'message' + from = @{ + id = $recipientId name = $recipientName } conversation = @{ - id = $conversationId + id = $conversationId name = '' } - recipient = @{ - id = $fromId + recipient = @{ + id = $fromId name = $fromName } - text = "I don't know how to upload files to Teams yet but I'm learning." - replyToId = $activityId + text = "I don't know how to upload files to Teams yet but I'm learning." + replyToId = $activityId } | ConvertTo-Json # $jsonResponse | Out-File -FilePath "$script:moduleBase/responses.json" -Append @@ -538,28 +538,75 @@ class TeamsBackend : Backend { # Populate the list of users the team [void]LoadUsers() { if (-not [string]::IsNullOrEmpty($this.ServiceUrl)) { - $this.LogDebug('Getting Teams users') + $this.LogInfo('Getting Teams users') - $uri = "$($this.ServiceUrl)v3/conversations/$($this.TeamId)/members/" + #Load Members from Teams + $uri = "$($this.ServiceUrl)v3/conversations/$($this.TeamId)/pagedmembers?pageSize=500" $headers = @{ Authorization = "Bearer $($this.Connection._AccessTokenInfo.access_token)" } - $members = Invoke-RestMethod -Uri $uri -Headers $headers + + #Load Members from Cached File + + $poshBotDirTeams = Join-Path -Path $env:USERPROFILE -ChildPath '.poshbot.teams' + $OfflineTeamsUsersFileName = 'OfflineTeamsUsers.xml' + $offlineTeamsUsersFile = Join-Path $poshBotDirTeams $OfflineTeamsUsersFileName + + if ((Test-Path $offlineTeamsUsersFile ) -and ((Get-ChildItem $offlineTeamsUsersFile).LastWriteTime -gt $(Get-Date).AddDays(-7)) -and ((Get-ChildItem $offlineTeamsUsersFile).Length -gt 1024)) { + #if (Test-Path $offlineTeamsUsersFile ) { + $this.LogInfo('[Teams] Getting Users (XML) - OFFLINE') + $members = @() + $members = Import-Clixml $offlineTeamsUsersFile + } else { + $this.LogInfo('[Teams] Getting Users (Teams) - Online') + + $members = @() + do { + $Results = '' + $StatusCode = '' + do { + try { + $Results = Invoke-RestMethod -Headers $headers -Uri $Uri -UseBasicParsing -Method 'GET' -ContentType 'application/json' + + $StatusCode = $Results.StatusCode + } catch { + $StatusCode = $_.Exception.Response.StatusCode.value__ + + if ($StatusCode -eq 429) { + $this.LogDebug('Got throttled by Microsoft. Sleeping for 45 seconds...') + Start-Sleep -Seconds 45 + } else { + $this.LogDebug("Error Populating the list of users for the team: $($_.Exception.Message)") + } + } + } while ($StatusCode -eq 429) + if ($Results.continuationToken) { + $uri = "$($this.ServiceUrl)v3/conversations/$($this.TeamId)/pagedmembers?pageSize=500&continuationToken=$($Results.continuationToken)" + $members += $Results.members + } else { + $members += $Results.members + } + } while ($Results.continuationToken) + + $members | Export-Clixml $offlineTeamsUsersFile + } + + $this.LogInfo('[Teams] Users: ' + $members.count) $this.LogDebug('Finished getting Teams users') - $members | Foreach-Object { + $members | ForEach-Object { $user = [TeamsPerson]::new() - $user.Id = $_.id - $user.FirstName = $_.givenName - $user.LastName = $_.surname - $user.NickName = $_.userPrincipalName - $user.FullName = "$($_.givenName) $($_.surname)" - $user.Email = $_.email + $user.Id = $_.id + $user.FirstName = $_.givenName + $user.LastName = $_.surname + $user.NickName = $_.userPrincipalName + $user.FullName = "$($_.givenName) $($_.surname)" + $user.Email = $_.email $user.UserPrincipalName = $_.userPrincipalName if (-not $this.Users.ContainsKey($_.ID)) { $this.LogDebug("Adding user [$($_.ID):$($_.Name)]") - $this.Users[$_.ID] = $user + $this.Users[$_.ID] = $user } } @@ -575,30 +622,30 @@ class TeamsBackend : Backend { # Populate the list of channels in the team [void]LoadRooms() { #if (-not [string]::IsNullOrEmpty($this.TeamId)) { - $this.LogDebug('Getting Teams channels') + $this.LogDebug('Getting Teams channels') - $uri = "$($this.ServiceUrl)v3/teams/$($this.TeamId)/conversations" - $headers = @{ - Authorization = "Bearer $($this.Connection._AccessTokenInfo.access_token)" + $uri = "$($this.ServiceUrl)v3/teams/$($this.TeamId)/conversations" + $headers = @{ + Authorization = "Bearer $($this.Connection._AccessTokenInfo.access_token)" + } + $channels = Invoke-RestMethod -Uri $uri -Headers $headers + + if ($channels.conversations) { + $channels.conversations | ForEach-Object { + $channel = [TeamsChannel]::new() + $channel.Id = $_.id + $channel.Name = $_.name + $this.LogDebug("Adding channel: $($_.id):$($_.name)") + $this.Rooms[$_.id] = $channel } - $channels = Invoke-RestMethod -Uri $uri -Headers $headers - - if ($channels.conversations) { - $channels.conversations | ForEach-Object { - $channel = [TeamsChannel]::new() - $channel.Id = $_.id - $channel.Name = $_.name - $this.LogDebug("Adding channel: $($_.id):$($_.name)") - $this.Rooms[$_.id] = $channel - } - foreach ($key in $this.Rooms.Keys) { - if ($key -notin $channels.conversations.ID) { - $this.LogDebug("Removing outdated channel [$key]") - $this.Rooms.Remove($key) - } + foreach ($key in $this.Rooms.Keys) { + if ($key -notin $channels.conversations.ID) { + $this.LogDebug("Removing outdated channel [$key]") + $this.Rooms.Remove($key) } } + } #} } @@ -626,7 +673,7 @@ class TeamsBackend : Backend { # Get a user Id by their name [string]UsernameToUserId([string]$Username) { $Username = $Username.TrimStart('@') - $user = $this.Users.Values | Where-Object {$_.Nickname -eq $Username} + $user = $this.Users.Values | Where-Object { $_.Nickname -eq $Username } $id = $null if ($user) { $id = $user.Id @@ -635,7 +682,7 @@ class TeamsBackend : Backend { # Refresh it and try again $this.LogDebug([LogSeverity]::Warning, "User [$Username] not found. Refreshing users") $this.LoadUsers() - $user = $this.Users.Values | Where-Object {$_.Nickname -eq $Username} + $user = $this.Users.Values | Where-Object { $_.Nickname -eq $Username } if (-not $user) { $id = $null } else { @@ -733,7 +780,7 @@ class TeamsBackend : Backend { if ([string]::IsNullOrEmpty($this.BotId)) { if ($Message.recipient) { - $this.BotId = $Message.recipient.Id + $this.BotId = $Message.recipient.Id $this.BotName = $Message.recipient.name } } @@ -748,10 +795,10 @@ class TeamsBackend : Backend { } hidden [void]SendTeamsMessaage([Response]$Response) { - $baseUrl = $Response.OriginalMessage.RawMessage.serviceUrl + $baseUrl = $Response.OriginalMessage.RawMessage.serviceUrl $conversationId = $Response.OriginalMessage.RawMessage.conversation.id - $activityId = $Response.OriginalMessage.RawMessage.id - $responseUrl = "$($baseUrl)v3/conversations/$conversationId/activities/$activityId" + $activityId = $Response.OriginalMessage.RawMessage.id + $responseUrl = "$($baseUrl)v3/conversations/$conversationId/activities/$activityId" $headers = @{ Authorization = "Bearer $($this.Connection._AccessTokenInfo.access_token)" } @@ -759,21 +806,21 @@ class TeamsBackend : Backend { if ($Response.Text.Count -gt 0) { foreach ($text in $Response.Text) { $jsonResponse = @{ - type = 'message' - from = @{ - id = $Response.OriginalMessage.RawMessage.recipient.id + type = 'message' + from = @{ + id = $Response.OriginalMessage.RawMessage.recipient.id name = $Response.OriginalMessage.RawMessage.recipient.name } conversation = @{ - id = $Response.OriginalMessage.RawMessage.conversation.id + id = $Response.OriginalMessage.RawMessage.conversation.id name = '' } - recipient = @{ - id = $Response.OriginalMessage.RawMessage.from.id + recipient = @{ + id = $Response.OriginalMessage.RawMessage.from.id name = $Response.OriginalMessage.RawMessage.from.name } - text = $text - replyToId = $activityId + text = $text + replyToId = $activityId } | ConvertTo-Json # $jsonResponse | Out-File -FilePath "$script:moduleBase/responses.json" -Append @@ -806,11 +853,11 @@ class TeamsBackend : Backend { } $conversationParams = @{ - bot = @{ - id = $this.BotId + bot = @{ + id = $this.BotId name = $this.BotName } - members = @( + members = @( @{ id = $UserId } @@ -844,8 +891,8 @@ class TeamsBackend : Backend { hidden [hashtable]_GetCardStub() { return @{ - type = 'message' - from = @{ + type = 'message' + from = @{ id = $null name = $null } @@ -853,37 +900,37 @@ class TeamsBackend : Backend { id = $null #name = '' } - recipient = @{ - id = $null + recipient = @{ + id = $null name = $null } - attachments = @( + attachments = @( @{ contentType = 'application/vnd.microsoft.card.adaptive' - content = @{ - type = 'AdaptiveCard' - version = '1.0' + content = @{ + type = 'AdaptiveCard' + version = '1.0' fallbackText = $null - body = @( + body = @( @{ - type = 'Container' + type = 'Container' spacing = 'none' - items = @( + items = @( # # Title & Thumbnail row @{ - type = 'ColumnSet' + type = 'ColumnSet' spacing = 'none' columns = @() } # Text & image row @{ - type = 'ColumnSet' + type = 'ColumnSet' spacing = 'none' columns = @() } # Facts row @{ - type = 'FactSet' + type = 'FactSet' facts = @() } ) @@ -892,7 +939,7 @@ class TeamsBackend : Backend { } } ) - replyToId = $null + replyToId = $null } } diff --git a/PoshBot/PoshBot.psd1 b/PoshBot/PoshBot.psd1 index f7243f2d..9ba8f43b 100644 --- a/PoshBot/PoshBot.psd1 +++ b/PoshBot/PoshBot.psd1 @@ -1,10 +1,6 @@ # # Module manifest for module 'PoshBot' # -# Generated by: brand -# -# Generated on: 12/18/2016 -# @{ @@ -12,7 +8,7 @@ RootModule = 'PoshBot.psm1' # Version number of this module. -ModuleVersion = '0.14.0' +ModuleVersion = '0.14.32' # Supported PSEditions # CompatiblePSEditions = @() @@ -21,16 +17,16 @@ ModuleVersion = '0.14.0' GUID = '7bfb126c-b432-4921-989a-9802f525693f' # Author of this module -Author = 'Brandon Olin' +Author = 'Andrew V. Golubenkoff' # Company or vendor of this module -CompanyName = 'Community' +CompanyName = 'FUIB' # Copyright statement for this module -Copyright = '(c) Brandon Olin. All rights reserved.' +Copyright = '(c) Andrew V. Golubenkoff.' # Description of the functionality provided by this module -Description = 'A Powershell-based bot framework for ChatOps. PowerShell modules are loaded into PoshBot and instantly become available as bot commands. PoshBot currently supports connecting to Slack to provide you with awesome ChatOps goodness. Bot commands can optionally be secured via permissions, roles, and groups to control who can execute what.' +Description = 'FUIB Powershell-based bot. PowerShell modules are loaded into PoshBot and instantly become available as bot commands.' # Minimum version of the Windows PowerShell engine required by this module PowerShellVersion = '5.0' @@ -53,7 +49,7 @@ PowerShellVersion = '5.0' # Modules that must be imported into the global environment prior to importing this module RequiredModules = @( @{ModuleName = 'Configuration'; ModuleVersion = '1.3.1'} - @{ModuleName = 'PSSlack'; ModuleVersion = '1.0.2'} + @{ModuleName = 'PSSlack'; ModuleVersion = '1.0.6'} ) # Assemblies that must be loaded prior to importing this module @@ -87,6 +83,7 @@ FunctionsToExport = @( 'New-PoshBotMiddlewareHook' 'New-PoshBotScheduledTask' 'New-PoshBotSlackBackend' + 'New-PoshBotSlackAppSMBackend' 'New-PoshBotTeamsBackend' 'New-HelloPlugin' 'New-PoshBotCardResponse' @@ -125,16 +122,16 @@ PrivateData = @{ Tags = @('PoshBot', 'ChatOps', 'Bot') # A URL to the license for this module. - LicenseUri = 'https://raw.githubusercontent.com/poshbotio/PoshBot/master/LICENSE' + LicenseUri = 'https://raw.githubusercontent.com/golubenkoff/PoshBot/master/LICENSE' # A URL to the main website for this project. - ProjectUri = 'https://github.com/poshbotio/PoshBot' + ProjectUri = 'https://github.com/golubenkoff/PoshBot' # A URL to an icon representing this module. - IconUri = 'https://raw.githubusercontent.com/poshbotio/PoshBot/master/Media/poshbot_logo_thumb_256.png' + IconUri = 'https://raw.githubusercontent.com/golubenkoff/PoshBot/master/Media/poshbot_logo_thumb_256.png' # ReleaseNotes of this module - ReleaseNotes = 'https://raw.githubusercontent.com/poshbotio/PoshBot/master/CHANGELOG.md' + ReleaseNotes = 'https://raw.githubusercontent.com/golubenkoff/PoshBot/master/CHANGELOG.md' } # End of PSData hashtable } # End of PrivateData hashtable diff --git a/PoshBot/Public/New-PoshBotCardResponse.ps1 b/PoshBot/Public/New-PoshBotCardResponse.ps1 index 78a17f82..c91ec009 100644 --- a/PoshBot/Public/New-PoshBotCardResponse.ps1 +++ b/PoshBot/Public/New-PoshBotCardResponse.ps1 @@ -132,7 +132,9 @@ function New-PoshBotCardResponse { })] [string]$Color = '#D3D3D3', - [object]$CustomData + [object]$CustomData, + + [switch]$TH ) $response = [ordered]@{ @@ -141,6 +143,7 @@ function New-PoshBotCardResponse { Text = $Text.Trim() Private = $PSBoundParameters.ContainsKey('Private') DM = $PSBoundParameters['DM'] + TH = $PSBoundParameters['TH'] } if ($PSBoundParameters.ContainsKey('Title')) { $response.Title = $Title diff --git a/PoshBot/Public/New-PoshBotTextResponse.ps1 b/PoshBot/Public/New-PoshBotTextResponse.ps1 index bb3fb5e0..a04c7ceb 100644 --- a/PoshBot/Public/New-PoshBotTextResponse.ps1 +++ b/PoshBot/Public/New-PoshBotTextResponse.ps1 @@ -42,7 +42,9 @@ function New-PoshBotTextResponse { [switch]$AsCode, - [switch]$DM + [switch]$DM, + + [switch]$TH ) process { @@ -52,6 +54,7 @@ function New-PoshBotTextResponse { Text = $item.Trim() AsCode = $PSBoundParameters.ContainsKey('AsCode') DM = $PSBoundParameters.ContainsKey('DM') + TH = $PSBoundParameters.ContainsKey('TH') } } } diff --git a/docs/reference/functions/get-poshbot.md b/docs/reference/functions/Get-PoshBot.md similarity index 100% rename from docs/reference/functions/get-poshbot.md rename to docs/reference/functions/Get-PoshBot.md diff --git a/docs/reference/functions/get-poshbotconfiguration.md b/docs/reference/functions/Get-PoshBotConfiguration.md similarity index 100% rename from docs/reference/functions/get-poshbotconfiguration.md rename to docs/reference/functions/Get-PoshBotConfiguration.md diff --git a/docs/reference/functions/new-poshbotcardresponse.md b/docs/reference/functions/New-PoshBotCardResponse.md similarity index 87% rename from docs/reference/functions/new-poshbotcardresponse.md rename to docs/reference/functions/New-PoshBotCardResponse.md index 04e27926..1ac67941 100644 --- a/docs/reference/functions/new-poshbotcardresponse.md +++ b/docs/reference/functions/New-PoshBotCardResponse.md @@ -1,6 +1,6 @@ --- external help file: PoshBot-help.xml -Module Name: poshbot +Module Name: PoshBot online version: schema: 2.0.0 --- @@ -15,7 +15,7 @@ Tells PoshBot to send a specially formatted response. ``` New-PoshBotCardResponse [[-Type] ] [-DM] [[-Text] ] [[-Title] ] [[-ThumbnailUrl] ] [[-ImageUrl] ] [[-LinkUrl] ] [[-Fields] ] - [[-Color] ] [[-CustomData] ] [] + [[-Color] ] [[-CustomData] ] [-TH] [] ``` ## DESCRIPTION @@ -28,14 +28,15 @@ to craft a specially formatted message when sending back to the chat network. ### EXAMPLE 1 ``` function Do-Something { - [cmdletbinding()] +``` + +\[cmdletbinding()\] param( - [parameter(mandatory)] - [string]$MyParam + \[parameter(mandatory)\] + \[string\]$MyParam ) -``` -New-PoshBotCardResponse -Type Normal -Text 'OK, I did something.' -ThumbnailUrl 'https://www.streamsports.com/images/icon_green_check_256.png' + New-PoshBotCardResponse -Type Normal -Text 'OK, I did something.' -ThumbnailUrl 'https://www.streamsports.com/images/icon_green_check_256.png' } Tells PoshBot to send a formatted response back to the chat network. @@ -45,14 +46,15 @@ with a green border on the left, some text and a green checkmark thumbnail image ### EXAMPLE 2 ``` function Do-Something { - [cmdletbinding()] +``` + +\[cmdletbinding()\] param( - [parameter(mandatory)] - [string]$ComputerName + \[parameter(mandatory)\] + \[string\]$ComputerName ) -``` -$info = Get-ComputerInfo -ComputerName $ComputerName -ErrorAction SilentlyContinue + $info = Get-ComputerInfo -ComputerName $ComputerName -ErrorAction SilentlyContinue if ($info) { $fields = \[ordered\]@{ Name = $ComputerName @@ -234,6 +236,21 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -TH +{{ Fill TH Description }} + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). diff --git a/docs/reference/functions/new-poshbotconfiguration.md b/docs/reference/functions/New-PoshBotConfiguration.md similarity index 99% rename from docs/reference/functions/new-poshbotconfiguration.md rename to docs/reference/functions/New-PoshBotConfiguration.md index 46b4725d..229b5bac 100644 --- a/docs/reference/functions/new-poshbotconfiguration.md +++ b/docs/reference/functions/New-PoshBotConfiguration.md @@ -1,6 +1,6 @@ --- external help file: PoshBot-help.xml -Module Name: poshbot +Module Name: PoshBot online version: schema: 2.0.0 --- @@ -61,15 +61,16 @@ Create a new PoshBot configuration with default values except for the bot name a ### EXAMPLE 2 ``` $backend = @{Name = 'SlackBackend'; Token = 'xoxb-569733935137-njOPkyBThqOTTUnCZb7tZpKK'} -PS C:\> $botParams = @{ +``` + +PS C:\\\> $botParams = @{ Name = 'HAL9000' LogLevel = 'Info' BotAdmins = @('JoeUser') BackendConfiguration = $backend } -PS C:\> $myBotConfig = New-PoshBotConfiguration @botParams -PS C:\> $myBotConfig -``` +PS C:\\\> $myBotConfig = New-PoshBotConfiguration @botParams +PS C:\\\> $myBotConfig Name : HAL9000 ConfigurationDirectory : C:\Users\brand\.poshbot diff --git a/docs/reference/functions/New-PoshBotDiscordBackend.md b/docs/reference/functions/New-PoshBotDiscordBackend.md index 1e35d08d..865d6fc0 100644 --- a/docs/reference/functions/New-PoshBotDiscordBackend.md +++ b/docs/reference/functions/New-PoshBotDiscordBackend.md @@ -1,6 +1,6 @@ --- external help file: PoshBot-help.xml -Module Name: poshbot +Module Name: PoshBot online version: schema: 2.0.0 --- @@ -24,14 +24,15 @@ Create a new instance of a Discord backend ### EXAMPLE 1 ``` $backendConfig = @{ - Name = 'DiscordBackend' - Token = '' - ClientId = '' - GuildId = '' -} -PS C:\> $backend = New-PoshBotDiscordBackend -Configuration $backendConfig ``` +Name = 'DiscordBackend' + Token = '\' + ClientId = '\' + GuildId = '\' +} +PS C:\\\> $backend = New-PoshBotDiscordBackend -Configuration $backendConfig + Create a Discord backend using the specified connection information. ## PARAMETERS diff --git a/docs/reference/functions/New-PoshBotFileUpload.md b/docs/reference/functions/New-PoshBotFileUpload.md index e2f5f62b..65ad568b 100644 --- a/docs/reference/functions/New-PoshBotFileUpload.md +++ b/docs/reference/functions/New-PoshBotFileUpload.md @@ -1,6 +1,6 @@ --- external help file: PoshBot-help.xml -Module Name: poshbot +Module Name: PoshBot online version: schema: 2.0.0 --- @@ -40,11 +40,12 @@ the contents the bot command returns are sensitive and should not be visible to ### EXAMPLE 1 ``` function Do-Stuff { - [cmdletbinding()] - param() ``` -$myObj = \[pscustomobject\]@{ +\[cmdletbinding()\] + param() + + $myObj = \[pscustomobject\]@{ value1 = 'foo' value2 = 'bar' } @@ -60,11 +61,12 @@ Export a CSV file and tell PoshBot to upload the file back to the channel that i ### EXAMPLE 2 ``` function Get-SecretPlan { - [cmdletbinding()] - param() ``` -$myObj = \[pscustomobject\]@{ +\[cmdletbinding()\] + param() + + $myObj = \[pscustomobject\]@{ Title = 'Secret moon base' Description = 'Plans for secret base on the dark side of the moon' } @@ -80,11 +82,12 @@ Export a CSV file and tell PoshBot to upload the file back to a DM channel with ### EXAMPLE 3 ``` function Do-Stuff { - [cmdletbinding()] - param() ``` -$myObj = \[pscustomobject\]@{ +\[cmdletbinding()\] + param() + + $myObj = \[pscustomobject\]@{ value1 = 'foo' value2 = 'bar' } diff --git a/docs/reference/functions/new-poshbotinstance.md b/docs/reference/functions/New-PoshBotInstance.md similarity index 91% rename from docs/reference/functions/new-poshbotinstance.md rename to docs/reference/functions/New-PoshBotInstance.md index 756c54a8..0f018e8e 100644 --- a/docs/reference/functions/new-poshbotinstance.md +++ b/docs/reference/functions/New-PoshBotInstance.md @@ -1,6 +1,6 @@ --- external help file: PoshBot-help.xml -Module Name: poshbot +Module Name: PoshBot online version: schema: 2.0.0 --- @@ -56,11 +56,12 @@ Create a new PoshBot instance from configuration file \[C:\Users\joeuser\.poshbo ### EXAMPLE 2 ``` $botConfig = Get-PoshBotConfiguration -Path (Join-Path -Path $env:USERPROFILE -ChildPath '.poshbot\Cherry2000.psd1') -PS C:\> $backend = New-PoshBotSlackBackend -Configuration $botConfig.BackendConfiguration -PS C:\> $myBot = $botConfig | New-PoshBotInstance -Backend $backend -PS C:\> $myBot | Format-List ``` +PS C:\\\> $backend = New-PoshBotSlackBackend -Configuration $botConfig.BackendConfiguration +PS C:\\\> $myBot = $botConfig | New-PoshBotInstance -Backend $backend +PS C:\\\> $myBot | Format-List + Name : Cherry2000 Backend : SlackBackend Storage : StorageProvider @@ -75,10 +76,11 @@ Gets a bot configuration from the filesytem, creates a chat backend object, and ### EXAMPLE 3 ``` $botConfig = Get-PoshBotConfiguration -Path (Join-Path -Path $env:USERPROFILE -ChildPath '.poshbot\Cherry2000.psd1') -PS C:\> $backend = $botConfig | New-PoshBotSlackBackend -PS C:\> $myBotJob = $botConfig | New-PoshBotInstance -Backend $backend | Start-PoshBot -AsJob -PassThru ``` +PS C:\\\> $backend = $botConfig | New-PoshBotSlackBackend +PS C:\\\> $myBotJob = $botConfig | New-PoshBotInstance -Backend $backend | Start-PoshBot -AsJob -PassThru + Gets a bot configuration, creates a Slack backend from it, then creates a new PoshBot instance and starts it as a background job. ## PARAMETERS diff --git a/docs/reference/functions/New-PoshBotMiddlewareHook.md b/docs/reference/functions/New-PoshBotMiddlewareHook.md index 0e9eadc9..c3ca5730 100644 --- a/docs/reference/functions/New-PoshBotMiddlewareHook.md +++ b/docs/reference/functions/New-PoshBotMiddlewareHook.md @@ -1,6 +1,6 @@ --- external help file: PoshBot-help.xml -Module Name: poshbot +Module Name: PoshBot online version: schema: 2.0.0 --- @@ -28,9 +28,10 @@ Middleware gets executed in the order in which it is added under each property. ### EXAMPLE 1 ``` $userDropHook = New-PoshBotMiddlewareHook -Name 'dropuser' -Path 'c:/poshbot/middleware/dropuser.ps1' -PS C:\> $config.MiddlewareConfiguration.Add($userDropHook, 'PreReceive') ``` +PS C:\\\> $config.MiddlewareConfiguration.Add($userDropHook, 'PreReceive') + Creates a middleware hook called 'dropuser' and adds it to the 'PreReceive' middleware lifecycle stage. ## PARAMETERS diff --git a/docs/reference/functions/new-poshbotscheduledtask.md b/docs/reference/functions/New-PoshBotScheduledTask.md similarity index 94% rename from docs/reference/functions/new-poshbotscheduledtask.md rename to docs/reference/functions/New-PoshBotScheduledTask.md index 3f1c8e49..e4e93c07 100644 --- a/docs/reference/functions/new-poshbotscheduledtask.md +++ b/docs/reference/functions/New-PoshBotScheduledTask.md @@ -1,6 +1,6 @@ --- external help file: PoshBot-help.xml -Module Name: poshbot +Module Name: PoshBot online version: schema: 2.0.0 --- @@ -27,25 +27,27 @@ to run on startup and to not stop after any time period. ### EXAMPLE 1 ``` $cred = Get-Credential -PS C:\> New-PoshBotScheduledTask -Name PoshBot -Path C:\PoshBot\myconfig.psd1 -Credential $cred ``` +PS C:\\\> New-PoshBotScheduledTask -Name PoshBot -Path C:\PoshBot\myconfig.psd1 -Credential $cred + Creates a new scheduled task to start PoshBot using the configuration file located at C:\PoshBot\myconfig.psd1 and the specified credential. ### EXAMPLE 2 ``` $cred = Get-Credential -PC C:\> $params = @{ +``` + +PC C:\\\> $params = @{ Name = 'PoshBot' Path = 'C:\PoshBot\myconfig.psd1' Credential = $cred Description = 'Awesome ChatOps bot' PassThru = $true } -PS C:\> $task = New-PoshBotScheduledTask @params -PS C:\> $task | Start-ScheduledTask -``` +PS C:\\\> $task = New-PoshBotScheduledTask @params +PS C:\\\> $task | Start-ScheduledTask Creates a new scheduled task to start PoshBot using the configuration file located at C:\PoshBot\myconfig.psd1 and the specified credential then starts the task. diff --git a/docs/reference/functions/New-PoshBotSlackAppSMBackend.md b/docs/reference/functions/New-PoshBotSlackAppSMBackend.md new file mode 100644 index 00000000..23bc73aa --- /dev/null +++ b/docs/reference/functions/New-PoshBotSlackAppSMBackend.md @@ -0,0 +1,61 @@ +--- +external help file: PoshBot-help.xml +Module Name: PoshBot +online version: +schema: 2.0.0 +--- + +# New-PoshBotSlackAppSMBackend + +## SYNOPSIS +Create a new instance of a Slack backend + +## SYNTAX + +``` +New-PoshBotSlackAppSMBackend [-Configuration] [] +``` + +## DESCRIPTION +Create a new instance of a Slack backend + +## EXAMPLES + +### EXAMPLE 1 +``` +$backendConfig = @{Name = 'SlackBackend'; Token = ''} +``` + +PS C:\\\> $backend = New-PoshBotSlackBackend -Configuration $backendConfig + +Create a Slack backend using the specified API token + +## PARAMETERS + +### -Configuration +The hashtable containing backend-specific properties on how to create the Slack backend instance. + +```yaml +Type: Hashtable[] +Parameter Sets: (All) +Aliases: BackendConfiguration + +Required: True +Position: 1 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### Hashtable +## OUTPUTS + +### SlackBackend +## NOTES + +## RELATED LINKS diff --git a/docs/reference/functions/new-poshbotslackbackend.md b/docs/reference/functions/New-PoshBotSlackBackend.md similarity index 92% rename from docs/reference/functions/new-poshbotslackbackend.md rename to docs/reference/functions/New-PoshBotSlackBackend.md index 3b49aed8..5dfc4381 100644 --- a/docs/reference/functions/new-poshbotslackbackend.md +++ b/docs/reference/functions/New-PoshBotSlackBackend.md @@ -1,6 +1,6 @@ --- external help file: PoshBot-help.xml -Module Name: poshbot +Module Name: PoshBot online version: schema: 2.0.0 --- @@ -24,9 +24,10 @@ Create a new instance of a Slack backend ### EXAMPLE 1 ``` $backendConfig = @{Name = 'SlackBackend'; Token = ''} -PS C:\> $backend = New-PoshBotSlackBackend -Configuration $backendConfig ``` +PS C:\\\> $backend = New-PoshBotSlackBackend -Configuration $backendConfig + Create a Slack backend using the specified API token ## PARAMETERS diff --git a/docs/reference/functions/New-PoshBotTeamsBackend.md b/docs/reference/functions/New-PoshBotTeamsBackend.md index 8476afe9..f5448343 100644 --- a/docs/reference/functions/New-PoshBotTeamsBackend.md +++ b/docs/reference/functions/New-PoshBotTeamsBackend.md @@ -1,6 +1,6 @@ --- external help file: PoshBot-help.xml -Module Name: poshbot +Module Name: PoshBot online version: schema: 2.0.0 --- @@ -24,18 +24,19 @@ Create a new instance of a Microsoft Teams backend ### EXAMPLE 1 ``` $backendConfig = @{ - Name = 'TeamsBackend' - Credential = [pscredential]::new( - '', - ('' | ConvertTo-SecureString -AsPlainText -Force) +``` + +Name = 'TeamsBackend' + Credential = \[pscredential\]::new( + '\', + ('\' | ConvertTo-SecureString -AsPlainText -Force) ) - ServiceBusNamespace = '' - QueueName = '' - AccessKeyName = '' - AccessKey = '' | ConvertTo-SecureString -AsPlainText -Force + ServiceBusNamespace = '\' + QueueName = '\' + AccessKeyName = '\' + AccessKey = '\' | ConvertTo-SecureString -AsPlainText -Force } -PS C:\> $$backend = New-PoshBotTeamsBackend -Configuration $backendConfig -``` +PS C:\\\> $$backend = New-PoshBotTeamsBackend -Configuration $backendConfig Create a Microsoft Teams backend using the specified Bot Framework credentials and Service Bus information diff --git a/docs/reference/functions/new-poshbottextresponse.md b/docs/reference/functions/New-PoshBotTextResponse.md similarity index 82% rename from docs/reference/functions/new-poshbottextresponse.md rename to docs/reference/functions/New-PoshBotTextResponse.md index 2f783abb..7cd38b8b 100644 --- a/docs/reference/functions/new-poshbottextresponse.md +++ b/docs/reference/functions/New-PoshBotTextResponse.md @@ -1,6 +1,6 @@ --- external help file: PoshBot-help.xml -Module Name: poshbot +Module Name: PoshBot online version: schema: 2.0.0 --- @@ -13,7 +13,7 @@ Tells PoshBot to handle the text response from a command in a special way. ## SYNTAX ``` -New-PoshBotTextResponse [-Text] [-AsCode] [-DM] [] +New-PoshBotTextResponse [-Text] [-AsCode] [-DM] [-TH] [] ``` ## DESCRIPTION @@ -27,14 +27,15 @@ in the channel. ### EXAMPLE 1 ``` function Get-Foo { - [cmdletbinding()] +``` + +\[cmdletbinding()\] param( - [parameter(mandatory)] - [string]$MyParam + \[parameter(mandatory)\] + \[string\]$MyParam ) -``` -New-PoshBotTextResponse -Text $MyParam -DM + New-PoshBotTextResponse -Text $MyParam -DM } When Get-Foo is executed by PoshBot, the text response will be sent back to the calling user as a DM rather than back in the channel the @@ -89,6 +90,21 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -TH +{{ Fill TH Description }} + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). diff --git a/docs/reference/functions/PoshBot.md b/docs/reference/functions/PoshBot.md index ede4c6a8..da89c44e 100644 --- a/docs/reference/functions/PoshBot.md +++ b/docs/reference/functions/PoshBot.md @@ -41,6 +41,9 @@ Locale: en-US ### [New-PoshBotScheduledTask](New-PoshBotScheduledTask.md) {{ Fill in the Description }} +### [New-PoshBotSlackAppSMBackend](New-PoshBotSlackAppSMBackend.md) +{{ Fill in the Description }} + ### [New-PoshBotSlackBackend](New-PoshBotSlackBackend.md) {{ Fill in the Description }} diff --git a/docs/reference/functions/save-poshbotconfiguration.md b/docs/reference/functions/Save-PoshBotConfiguration.md similarity index 100% rename from docs/reference/functions/save-poshbotconfiguration.md rename to docs/reference/functions/Save-PoshBotConfiguration.md diff --git a/docs/reference/functions/start-poshbot.md b/docs/reference/functions/Start-PoshBot.md similarity index 98% rename from docs/reference/functions/start-poshbot.md rename to docs/reference/functions/Start-PoshBot.md index d3775e23..5fe90596 100644 --- a/docs/reference/functions/start-poshbot.md +++ b/docs/reference/functions/Start-PoshBot.md @@ -1,6 +1,6 @@ --- external help file: PoshBot-help.xml -Module Name: poshbot +Module Name: PoshBot online version: schema: 2.0.0 --- @@ -49,9 +49,10 @@ Runs an instance of PoshBot that has already been created interactively in the s ### EXAMPLE 3 ``` $config = Get-PoshBotConfiguration -Path (Join-Path -Path $env:USERPROFILE -ChildPath '.poshbot\MyPoshBot.psd1') -PS C:\> Start-PoshBot -Config $config ``` +PS C:\\\> Start-PoshBot -Config $config + Gets a PoshBot configuration from file and starts the bot interactively. ### EXAMPLE 4 diff --git a/docs/reference/functions/stop-poshbot.md b/docs/reference/functions/Stop-Poshbot.md similarity index 100% rename from docs/reference/functions/stop-poshbot.md rename to docs/reference/functions/Stop-Poshbot.md diff --git a/psakeFile.ps1 b/psakeFile.ps1 index 2a334af5..25da2062 100644 --- a/psakeFile.ps1 +++ b/psakeFile.ps1 @@ -57,6 +57,7 @@ task Analyze -Depends Build { } -description 'Run PSScriptAnalyzer' task Pester -Depends Build { + Import-Module Pester -RequiredVersion 3.4.0 Push-Location Set-Location -PassThru $outputModDir if(-not $ENV:BHProjectPath) { diff --git a/requirements.psd1 b/requirements.psd1 index 209468ec..54cdce9f 100644 --- a/requirements.psd1 +++ b/requirements.psd1 @@ -7,6 +7,6 @@ Configuration = '1.3.1' Pester = '4.9.0' PSScriptAnalyzer = '1.18.3' - PSSlack = '1.0.2' + PSSlack = '1.0.6' platyPS = '0.14.0' }