diff --git a/help/literate/templates/template-project.html b/help/literate/templates/template-project.html index 288a133e7fc..56350b57cfb 100644 --- a/help/literate/templates/template-project.html +++ b/help/literate/templates/template-project.html @@ -80,6 +80,7 @@

{project-name}

  • WiX Setup Generation
  • Using Chocolatey
  • Using Slack
  • +
  • Using Microsoft Teams
  • Using SonarQube
  • Fake.Deploy
  • diff --git a/help/msteamsnotification.md b/help/msteamsnotification.md new file mode 100644 index 00000000000..3d11f73768b --- /dev/null +++ b/help/msteamsnotification.md @@ -0,0 +1,52 @@ +# Sending Notifications to a Microsoft Teams channel + +In this article you will learn how to create a Office 365 webhook integration on a MS Teams channel and send a notification to it. This article assumes that you already have a Microsoft Teams team and channel setup. + +## Adding a Webhook Integration to a Channel + +Follow the [instructions](https://msdn.microsoft.com/en-us/microsoft-teams/connectors) for setting up a webhook connector to your channel. When finished, you should have a Webhook URL that looks like "https://outlook.office.com/webhook/some-random-text/IncomingWebhook/some/random/text". + +## Sending a Notification to the Webhook + + // The webhook URL from the integration you set up + let webhookUrl = "https://outlook.office.com/webhook/some-random-text/IncomingWebhook/some/random/text" + + let imageUrl = sprintf "https://connectorsdemo.azurewebsites.net/images/%s" + + let notification p = + { p with + Summary = Some "Max Muster ran a build" + Title = Some "Sample Project" + Sections = + [ { SectionDefaults with + ActivityTitle = Some "Max Muster" + ActivitySubtitle = Some "on Sample Project" + ActivityText = Some "Build successful!" + ActivityImage = + imageUrl "MSC12_Oscar_002.jpg" + |> ImageUri.FromUrl + |> Some + } + { SectionDefaults with + Title = Some "Details" + Facts = [ { Name = "Labels"; Value = "FOO, BAR" } + { Name = "Version"; Value = "1.0.0" } + { Name = "Trello Id"; Value = "1101" } ] + } + ] + PotentialActions = + [ + { + Name = "View in Trello" + Target = System.Uri("https://trello.com/c/1101/") + } + ] + } + + Office365Notification webhookUrl notification |> ignore + +The result should look something like this: + +![alt text](pics/msteamsnotification/msteamsnotification.png "Microsoft Teams Notification Result") + +For additional information on the parameters, check out the [Office 356 Connectors API Reference](https://dev.outlook.com/connectors/reference) diff --git a/help/pics/msteamsnotification/msteamsnotification.png b/help/pics/msteamsnotification/msteamsnotification.png new file mode 100644 index 00000000000..5738196356a Binary files /dev/null and b/help/pics/msteamsnotification/msteamsnotification.png differ diff --git a/help/templates/template.cshtml b/help/templates/template.cshtml index 3d2d3ee50b5..a8b7679f9e5 100644 --- a/help/templates/template.cshtml +++ b/help/templates/template.cshtml @@ -81,6 +81,7 @@
  • WiX Setup Generation
  • Using Chocolatey
  • Using Slack
  • +
  • Using Microsoft Teams
  • Using SonarQube
  • Fake.Deploy
  • diff --git a/src/app/FakeLib/FakeLib.fsproj b/src/app/FakeLib/FakeLib.fsproj index 4b291618eb8..923bba73c5d 100644 --- a/src/app/FakeLib/FakeLib.fsproj +++ b/src/app/FakeLib/FakeLib.fsproj @@ -192,6 +192,7 @@ + diff --git a/src/app/FakeLib/Office365ConnectorHelper.fs b/src/app/FakeLib/Office365ConnectorHelper.fs new file mode 100644 index 00000000000..0a7c19cbe28 --- /dev/null +++ b/src/app/FakeLib/Office365ConnectorHelper.fs @@ -0,0 +1,357 @@ +/// Contains a task to send notification messages to a [Office 356 Connector](https://dev.outlook.com/connectors/reference) webhook +/// +/// ## Sample +/// +/// let imageUrl = sprintf "https://connectorsdemo.azurewebsites.net/images/%s" +/// +/// let notification p = +/// { p with +/// Summary = Some "Max Muster ran a build" +/// Title = Some "Sample Project" +/// Sections = +/// [ { SectionDefaults with +/// ActivityTitle = Some "Max Muster" +/// ActivitySubtitle = Some "on Sample Project" +/// ActivityImage = +/// imageUrl "MSC12_Oscar_002.jpg" +/// |> ImageUri.FromUrl +/// |> Some +/// } +/// { SectionDefaults with +/// Title = Some "Details" +/// Facts = [ { Name = "Labels"; Value = "FOO, BAR" } +/// { Name = "Version"; Value = "1.0.0" } +/// { Name = "Trello Id"; Value = "1101" } ] +/// } +/// ] +/// PotentialActions = +/// [ +/// { +/// Name = "View in Trello" +/// Target = System.Uri("https://trello.com/c/1101/") +/// } +/// ] +/// } +/// +/// let webhookURL = "" +/// +/// Office365Notification webhookURL notification |> ignore +/// + +module Fake.Office365ConnectorHelper + +open System.Net +open System +open Newtonsoft.Json + +/// This type alias for string gives you a hint where you can use markdown +type MarkdownString = string + +/// This type alias for string gives you a hint where you **can't** use markdown +type SimpleString = string + +/// This type alias gives you a hint where you have to use a Hex color value (e.g. #AAFF77) +type ColorHexValue = string + +/// [omit] +let inline private writeJson (w: JsonWriter) (x: ^T) = (^T: (member WriteJson: JsonWriter -> JsonWriter) x, w) + +/// [omit] +let private writePropertyName title (writer: JsonWriter) = + writer.WritePropertyName(title) + writer + +/// [omit] +let private writeString (value: string) (writer: JsonWriter) = + writer.WriteValue(value) + writer + +/// [omit] +let private writeNamedString title value (writer: JsonWriter) = + writer + |> writePropertyName title + |> writeString value + +/// [omit] +let private writeNonEmptyValue title value (writer: JsonWriter) = + match value with + | Some v when v |> isNotNullOrEmpty -> + writer + |> writePropertyName title + |> writeString v + | _ -> writer + +/// [omit] +let private asList title (writeValues: JsonWriter -> JsonWriter) (writer: JsonWriter) = + writer.WritePropertyName(title) + writer.WriteStartArray() + writer |> writeValues |> ignore + writer.WriteEndArray() + writer + +/// [omit] +let private asObject (writeValues: JsonWriter -> JsonWriter) (writer: JsonWriter) = + writer.WriteStartObject() + writer |> writeValues |> ignore + writer.WriteEndObject() + writer + +/// Represents an action button +type ViewAction = + { + /// (Required) The name of the Action (appears on the button). + Name: SimpleString + + /// (Required) The Url of the link for the button + Target: Uri + } + + /// Writes the action to a JSON writer + member self.WriteJson (writer: JsonWriter) = + writer |> asObject (fun _ -> + writer + |> writeNamedString "@context" "http://schema.org" + |> writeNamedString "@type" "ViewAction" + |> writeNamedString "name" self.Name + |> asList "target" (fun _ -> writer |> writeString (self.Target.ToString()))) + + +/// Represents the URI to an image (either a normal URI or a DataUri) +type ImageUri = + /// A simple URI of the image + | ImageUrl of Uri + + /// A Data uri of the image encoded as Base64 data + | DataUri of string + + /// Writes the image uri to a JSON writer + member self.WriteJson (writer: JsonWriter) = + match self with + | ImageUrl uri -> writer |> writeString (uri.ToString()) + | DataUri uri -> writer |> writeString uri + + /// Creates a new ImageUrl from a given url string + static member FromUrl url = + System.Uri(url) |> ImageUrl + + /// Creates a new DataUri from a given file + /// png, gif, jpg and bmp files are supported + static member FromFile fileName = + let allowedExtensions = [ "png"; "gif"; "jpg"; "bmp" ] + let extension = System.IO.Path.GetExtension(fileName) |> toLower + + match extension with + | ext when (allowedExtensions |> List.contains ext) -> + let imageBytes = System.IO.File.ReadAllBytes(fileName) + let data = Convert.ToBase64String(imageBytes, Base64FormattingOptions.None) + + DataUri (sprintf "data:image/%s,base64,%s" ext data) |> Some + | _ -> + traceError (sprintf "Extension \"%s\" is not supported!" extension) + None + +/// A simple key/value pair +type Fact = + { + /// (Required) Name of the fact + Name: SimpleString + + /// (Required) Value of the fact + Value: MarkdownString + } + + /// Writes the fact to a JSON writer + member self.WriteJson (writer: JsonWriter) = + writer |> asObject (fun _ -> + writer + |> writeNamedString "name" self.Name + |> writeNamedString "value" self.Value) + +/// Represents a described image object +type Image = + { + /// (Optional) Alt-text for the image + Title: SimpleString option + + /// (Required) A URL to the image file or a data URI with the base64-encoded image inline + Image: ImageUri + } + + static member FromUrlWithoutTitle url = + { Title = None + Image = url |> ImageUri.FromUrl } + + /// Writes the Image to a JSON writer + member self.WriteJson (writer: JsonWriter) = + writer |> asObject (fun w -> + w + |> writeNonEmptyValue "title" self.Title + |> writePropertyName "image" + |> self.Image.WriteJson) + +/// A section in a ConnectorCard +type Section = + { + /// (Optional) The title of the section + Title: MarkdownString option + + /// (Optional) Title of the event or action. Often this will be the name of the "actor". + ActivityTitle: MarkdownString option + + /// (Optional) A subtitle describing the event or action. Often this will be a summary of the action. + ActivitySubtitle: MarkdownString option + + /// (Optional) An image representing the action. Often this is an avatar of the "actor" of the activity. + ActivityImage: ImageUri option + + /// (Optional) A full description of the action. + ActivityText: MarkdownString option + + /// A list of facts, displayed as key-value pairs. + Facts: Fact list + + /// A list of images that will be displayed at the bottom of the section. + Images: Image list + + /// (Optional) A text that will appear before the activity. + Text: string option + + /// (Optional) Set this to false to disable markdown parsing on this section's content. Markdown parsing is enabled by default. + IsMarkdown: bool option + + /// This list of ViewAction objects will power the action links found at the bottom of the section + PotentialActions: ViewAction list + } + + /// Writes the Section to a JSON writer + member self.WriteJson (writer: JsonWriter) = + writer |> asObject (fun _ -> + writer + |> writeNonEmptyValue "title" self.Title + |> writeNonEmptyValue "activityTitle" self.ActivityTitle + |> writeNonEmptyValue "activitySubtitle" self.ActivitySubtitle + |> writeNonEmptyValue "activityText" self.ActivityText + |> fun _ -> match self.ActivityImage with + | Some i -> writer |> writePropertyName "activityImage" |> i.WriteJson + | _ -> writer + |> fun _ -> match self.Facts with + | [] -> writer + | _ -> writer |> asList "facts" (fun _ -> self.Facts |> List.fold writeJson writer) + + |> fun _ -> match self.Images with + | [] -> writer + | _ -> writer |> asList "images" (fun _ -> self.Images |> List.fold writeJson writer) + |> fun _ -> match self.IsMarkdown with + | Some false -> writer.WritePropertyName("markdown") + writer.WriteValue(false) + writer + | _ -> writer + |> fun _ -> match self.PotentialActions with + | [] -> writer + | _ -> writer |> asList "potentialAction" (fun _ -> self.PotentialActions |> List.fold writeJson writer)) + +/// This is the base data, which will be sent to the Office 365 webhook connector +type ConnectorCard = + { + /// (Required, if the text property is not populated) A string used for summarizing card content. This will be shown as the message subject. + Summary: SimpleString option + + /// (Optional) A title for the Connector message. Shown at the top of the message. + Title: SimpleString option + + /// The main text of the card. This will be rendered below the sender information and optional title, and above any sections or actions present. + Text: MarkdownString option + + /// (Optional) Accent color used for branding or indicating status in the card + ThemeColor: ColorHexValue option + + /// Contains a list of sections to display in the card + Sections: Section list + + /// This array of ViewAction objects will power the action links found at the bottom of the card + PotentialActions: ViewAction list + } + + member self.WriteJson (writer: JsonWriter) = + writer |> asObject (fun _ -> + writer + |> writeNonEmptyValue "summary" self.Summary + |> writeNonEmptyValue "title" self.Title + |> writeNonEmptyValue "text" self.Text + |> writeNonEmptyValue "themeColor" self.ThemeColor + |> fun _ -> match self.Sections with + | [] -> writer + | _ -> writer |> asList "sections" (fun _ -> self.Sections |> List.fold writeJson writer) + |> fun _ -> match self.PotentialActions with + | [] -> writer + | _ -> writer |> asList "potentialAction" (fun _ -> self.PotentialActions |> List.fold writeJson writer)) + + /// Converts the connector card to a JSON string + member self.AsJson() = + let sb = System.Text.StringBuilder() + let sw = new System.IO.StringWriter(sb) + use writer = new JsonTextWriter(sw) + writer |> self.WriteJson |> ignore + sb.ToString () + +/// Default values for a Section in a ConnectorCard (everything is empty here) +let SectionDefaults = + { + Title = None + ActivityTitle = None + ActivitySubtitle = None + ActivityImage = None + ActivityText = None + Facts = [] + Images = [] + Text = None + IsMarkdown = None + PotentialActions = [] + } + +/// Default values for a ConnectorCard (everything is empty here) +let ConnectorCardDefaults = + { + Summary = None + Title = None + Text = None + ThemeColor = None + Sections = [] + PotentialActions = [] + } + +/// [omit] +let private validateParams webhookURL (card : ConnectorCard) = + if webhookURL = "" then failwith "You must specify a webhook URL" + if card.Text.IsNone && card.Sections.Length = 0 then failwith "You must specify a message or include a section" + + let validateAction (action: ViewAction) = + if action.Name |> isNullOrEmpty then + failwith "You must specifiy a name for a ViewAction" + + let validateSection (section: Section) = + if section.Text.IsNone && section.ActivityText.IsNone && section.ActivityTitle.IsNone && section.Facts.Length = 0 && section.Images.Length = 0 then + failwith "You must specifiy a text or an activityText/activityTitle or some facts or some images in a section" + section.PotentialActions |> List.iter validateAction + () + + card.Sections |> List.iter validateSection + card.PotentialActions |> List.iter validateAction + + card + +/// Sends a notification to an Office 365 Connector +/// ## Parameters +/// - `webhookURL` - The Office 365 webhook connector URL +/// - `setParams` - Function used to override the default notification parameters +let Office365Notification (webhookURL : string) (setParams: ConnectorCard -> ConnectorCard) = + let sendNotification (card: ConnectorCard) = + use client = (new WebClient()) + + client.Headers.Add(HttpRequestHeader.ContentType, "application/json") + client.UploadString(webhookURL, "POST", card.AsJson ()) + + ConnectorCardDefaults + |> setParams + |> validateParams webhookURL + |> sendNotification