From 3ffe76b2d2f6c54f2c7201982156804f995a80b0 Mon Sep 17 00:00:00 2001 From: Jamie Date: Fri, 23 Mar 2018 11:29:38 +0000 Subject: [PATCH] Fixed #2055 and #1903 --- src/Ombi.Api.Mattermost/IMattermostApi.cs | 2 +- src/Ombi.Api.Mattermost/MattermostApi.cs | 10 +- .../Models/MattermostClient.cs | 168 ++++++++++++++++ .../Models/MattermostMessage.cs | 181 ++++++++++++++++++ .../Agents/MattermostNotification.cs | 40 ++-- 5 files changed, 381 insertions(+), 20 deletions(-) create mode 100644 src/Ombi.Api.Mattermost/Models/MattermostClient.cs create mode 100644 src/Ombi.Api.Mattermost/Models/MattermostMessage.cs diff --git a/src/Ombi.Api.Mattermost/IMattermostApi.cs b/src/Ombi.Api.Mattermost/IMattermostApi.cs index b07802b25..b8b77864c 100644 --- a/src/Ombi.Api.Mattermost/IMattermostApi.cs +++ b/src/Ombi.Api.Mattermost/IMattermostApi.cs @@ -5,6 +5,6 @@ namespace Ombi.Api.Mattermost { public interface IMattermostApi { - Task PushAsync(string webhook, MattermostBody message); + Task PushAsync(string webhook, MattermostMessage message); } } \ No newline at end of file diff --git a/src/Ombi.Api.Mattermost/MattermostApi.cs b/src/Ombi.Api.Mattermost/MattermostApi.cs index c20641aca..15a954b47 100644 --- a/src/Ombi.Api.Mattermost/MattermostApi.cs +++ b/src/Ombi.Api.Mattermost/MattermostApi.cs @@ -14,14 +14,10 @@ public MattermostApi(IApi api) private readonly IApi _api; - public async Task PushAsync(string webhook, MattermostBody message) + public async Task PushAsync(string webhook, MattermostMessage message) { - var request = new Request(string.Empty, webhook, HttpMethod.Post); - - request.AddJsonBody(message); - - var result = await _api.RequestContent(request); - return result; + var client = new MatterhookClient(webhook); + await client.PostAsync(_api, message); } } } diff --git a/src/Ombi.Api.Mattermost/Models/MattermostClient.cs b/src/Ombi.Api.Mattermost/Models/MattermostClient.cs new file mode 100644 index 000000000..96d3e33f4 --- /dev/null +++ b/src/Ombi.Api.Mattermost/Models/MattermostClient.cs @@ -0,0 +1,168 @@ +/// +/// +/// +/// Code taken from https://github.com/PromoFaux/Matterhook.NET.MatterhookClient +/// +/// +/// +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Ombi.Api.Mattermost.Models +{ + public class MatterhookClient + { + private readonly Uri _webhookUrl; + private readonly HttpClient _httpClient = new HttpClient(); + + /// + /// Create a new Mattermost Client + /// + /// The URL of your Mattermost Webhook + /// Timeout Value (Default 100) + public MatterhookClient(string webhookUrl, int timeoutSeconds = 100) + { + if (!Uri.TryCreate(webhookUrl, UriKind.Absolute, out _webhookUrl)) + throw new ArgumentException("Mattermost URL invalid"); + + _httpClient.Timeout = new TimeSpan(0, 0, 0, timeoutSeconds); + } + + public MattermostMessage CloneMessage(MattermostMessage inMsg) + { + var outMsg = new MattermostMessage + { + Text = "", + Channel = inMsg.Channel, + Username = inMsg.Username, + IconUrl = inMsg.IconUrl + }; + + return outMsg; + } + + private static MattermostAttachment CloneAttachment(MattermostAttachment inAtt) + { + var outAtt = new MattermostAttachment + { + AuthorIcon = inAtt.AuthorIcon, + AuthorLink = inAtt.AuthorLink, + AuthorName = inAtt.AuthorName, + Color = inAtt.Color, + Fallback = inAtt.Fallback, + Fields = inAtt.Fields, + ImageUrl = inAtt.ImageUrl, + Pretext = inAtt.Pretext, + ThumbUrl = inAtt.ThumbUrl, + Title = inAtt.Title, + TitleLink = inAtt.TitleLink, + Text = "" + }; + return outAtt; + } + + /// + /// Post Message to Mattermost server. Messages will be automatically split if total text length > 4000 + /// + /// + /// The messsage you wish to send + /// + public async Task PostAsync(IApi api, MattermostMessage inMessage) + { + try + { + var outMessages = new List(); + + var msgCount = 0; + + var lines = new string[] { }; + if (inMessage.Text != null) + { + lines = inMessage.Text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + } + + //start with one cloned inMessage in the list + outMessages.Add(CloneMessage(inMessage)); + + //add text from original. If we go over 3800, we'll split it to a new inMessage. + foreach (var line in lines) + { + + if (line.Length + outMessages[msgCount].Text.Length > 3800) + { + + msgCount += 1; + outMessages.Add(CloneMessage(inMessage)); + } + + outMessages[msgCount].Text += $"{line}\r\n"; + } + + //Length of text on the last (or first if only one) inMessage. + var lenMessageText = outMessages[msgCount].Text.Length; + + //does our original have attachments? + if (inMessage.Attachments?.Any() ?? false) + { + outMessages[msgCount].Attachments = new List(); + + //loop through them in a similar fashion to the inMessage text above. + foreach (var att in inMessage.Attachments) + { + //add this attachment to the outgoing message + outMessages[msgCount].Attachments.Add(CloneAttachment(att)); + //get a count of attachments on this message, and subtract one so we know the index of the current new attachment + var attIndex = outMessages[msgCount].Attachments.Count - 1; + + //Get the text lines + lines = att.Text.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + + foreach (var line in lines) + { + //Get the total length of all attachments on the current outgoing message + var lenAllAttsText = outMessages[msgCount].Attachments.Sum(a => a.Text.Length); + + if (lenMessageText + lenAllAttsText + line.Length > 3800) + { + msgCount += 1; + attIndex = 0; + outMessages.Add(CloneMessage(inMessage)); + outMessages[msgCount].Attachments = new List { CloneAttachment(att) }; + } + + outMessages[msgCount].Attachments[attIndex].Text += $"{line}\r\n"; + } + } + } + + + if (outMessages.Count > 1) + { + var num = 1; + foreach (var msg in outMessages) + { + msg.Text = $"`({num}/{msgCount + 1}): ` " + msg.Text; + num++; + } + } + + foreach (var msg in outMessages) + { + var request = new Request("", _webhookUrl.ToString(), HttpMethod.Post); + request.AddJsonBody(msg); + await api.Request(request); + } + } + catch (Exception e) + { + Console.WriteLine(e.Message); + throw; + } + } + } +} \ No newline at end of file diff --git a/src/Ombi.Api.Mattermost/Models/MattermostMessage.cs b/src/Ombi.Api.Mattermost/Models/MattermostMessage.cs new file mode 100644 index 000000000..d8fe52b09 --- /dev/null +++ b/src/Ombi.Api.Mattermost/Models/MattermostMessage.cs @@ -0,0 +1,181 @@ +/// +/// +/// +/// Code taken from https://github.com/PromoFaux/Matterhook.NET.MatterhookClient +/// +/// +/// +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Ombi.Api.Mattermost.Models + +{ + public class MattermostMessage + { + + //https://docs.mattermost.com/developer/webhooks-incoming.html + + /// + /// Channel to post to + /// + [JsonProperty(PropertyName = "channel")] + public string Channel { get; set; } + + /// + /// Username for bot + /// + [JsonProperty(PropertyName = "username")] + public string Username { get; set; } + + /// + /// Bot/User Icon + /// + [JsonProperty(PropertyName = "icon_url")] + public Uri IconUrl { get; set; } + + /// + /// Message body. Supports Markdown + /// + [JsonProperty(PropertyName = "text")] + public string Text { get; set; } + + /// + /// Richtext attachments + /// + [JsonProperty(PropertyName = "attachments")] + public List Attachments { get; set; } + + } + + /// + /// https://docs.mattermost.com/developer/message-attachments.html#message-attachments + /// + public class MattermostAttachment + { + //https://docs.mattermost.com/developer/message-attachments.html#attachment-options + #region AttachmentOptions + + /// + /// A required plain-text summary of the post. This is used in notifications, and in clients that don’t support formatted text (eg IRC). + /// + [JsonProperty(PropertyName = "fallback")] + public string Fallback { get; set; } + + /// + /// A hex color code that will be used as the left border color for the attachment. If not specified, it will default to match the left hand sidebar header background color. + /// + [JsonProperty(PropertyName = "color")] + public string Color { get; set; } + + /// + /// Optional text that should appear above the formatted data + /// + [JsonProperty(PropertyName = "pretext")] + public string Pretext { get; set; } + + /// + /// The text to be included in the attachment. It can be formatted using Markdown. If it includes more than 300 characters or more than 5 line breaks, the message will be collapsed and a “Show More” link will be added to expand the message. + /// + [JsonProperty(PropertyName = "text")] + public string Text { get; set; } + + #endregion + + //https://docs.mattermost.com/developer/message-attachments.html#author-details + #region AuthorDetails + + /// + /// An optional name used to identify the author. It will be included in a small section at the top of the attachment. + /// + [JsonProperty(PropertyName = "author_name")] + public string AuthorName { get; set; } + + /// + /// An optional URL used to hyperlink the author_name. If no author_name is specified, this field does nothing. + /// + [JsonProperty(PropertyName = "author_link")] + public Uri AuthorLink { get; set; } + + /// + /// An optional URL used to display a 16x16 pixel icon beside the author_name. + /// + [JsonProperty(PropertyName = "author_icon")] + public Uri AuthorIcon { get; set; } + + #endregion + + //https://docs.mattermost.com/developer/message-attachments.html#titles + #region Titles + + /// + /// An optional title displayed below the author information in the attachment. + /// + [JsonProperty(PropertyName = "title")] + public string Title { get; set; } + + /// + /// An optional URL used to hyperlink the title. If no title is specified, this field does nothing. + /// + [JsonProperty(PropertyName = "title_link")] + public Uri TitleLink { get; set; } + + #endregion + + + #region Fields + + /// + /// Fields can be included as an optional array within attachments, and are used to display information in a table format inside the attachment. + /// + [JsonProperty(PropertyName = "fields")] + public List Fields { get; set; } + + #endregion + + //https://docs.mattermost.com/developer/message-attachments.html#images + #region Images + + /// + /// An optional URL to an image file (GIF, JPEG, PNG, or BMP) that is displayed inside a message attachment. + /// Large images are resized to a maximum width of 400px or a maximum height of 300px, while still maintaining the original aspect ratio. + /// + [JsonProperty(PropertyName = "image_url")] + public Uri ImageUrl { get; set; } + + /// + /// An optional URL to an image file(GIF, JPEG, PNG, or BMP) that is displayed as a 75x75 pixel thumbnail on the right side of an attachment. + /// We recommend using an image that is already 75x75 pixels, but larger images will be scaled down with the aspect ratio maintained. + /// + [JsonProperty(PropertyName = "thumb_url")] + public Uri ThumbUrl { get; set; } + + + #endregion + } + + /// + /// https://docs.mattermost.com/developer/message-attachments.html#fieldshttps://docs.mattermost.com/developer/message-attachments.html#fields + /// + public class MattermostField + { + /// + /// A title shown in the table above the value. + /// + [JsonProperty(PropertyName = "title")] + public string Title { get; set; } + + /// + /// The text value of the field. It can be formatted using Markdown. + /// + [JsonProperty(PropertyName = "value")] + public string Value { get; set; } + + /// + /// Optionally set to “True” or “False” to indicate whether the value is short enough to be displayed beside other values. + /// + [JsonProperty(PropertyName = "short")] + public bool Short { get; set; } + } +} \ No newline at end of file diff --git a/src/Ombi.Notifications/Agents/MattermostNotification.cs b/src/Ombi.Notifications/Agents/MattermostNotification.cs index efc1df1a6..f07d62b72 100644 --- a/src/Ombi.Notifications/Agents/MattermostNotification.cs +++ b/src/Ombi.Notifications/Agents/MattermostNotification.cs @@ -59,10 +59,18 @@ protected override async Task NewRequest(NotificationOptions model, MattermostNo Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); + AddOtherInformation(model, notification, parsed); + //notification.Other.Add("overview", model.RequestType == RequestType.Movie ? base.MovieRequest.Overview : TvRequest.); await Send(notification, settings); } + private void AddOtherInformation(NotificationOptions model, NotificationMessage notification, + NotificationMessageContent parsed) + { + notification.Other.Add("image", parsed.Image); + notification.Other.Add("title", model.RequestType == RequestType.Movie ? MovieRequest.Title : TvRequest.Title); + } + protected override async Task NewIssue(NotificationOptions model, MattermostNotificationSettings settings) { var parsed = await LoadTemplate(NotificationAgent.Mattermost, NotificationType.Issue, model); @@ -75,7 +83,7 @@ protected override async Task NewIssue(NotificationOptions model, MattermostNoti { Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); + AddOtherInformation(model, notification, parsed); await Send(notification, settings); } @@ -91,7 +99,7 @@ protected override async Task IssueComment(NotificationOptions model, Mattermost { Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); + AddOtherInformation(model, notification, parsed); await Send(notification, settings); } @@ -107,7 +115,7 @@ protected override async Task IssueResolved(NotificationOptions model, Mattermos { Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); + AddOtherInformation(model, notification, parsed); await Send(notification, settings); } @@ -149,7 +157,7 @@ protected override async Task RequestDeclined(NotificationOptions model, Matterm { Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); + AddOtherInformation(model, notification, parsed); await Send(notification, settings); } @@ -166,7 +174,7 @@ protected override async Task RequestApproved(NotificationOptions model, Matterm Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); + AddOtherInformation(model, notification, parsed); await Send(notification, settings); } @@ -182,7 +190,7 @@ protected override async Task AvailableRequest(NotificationOptions model, Matter { Message = parsed.Message, }; - notification.Other.Add("image", parsed.Image); + AddOtherInformation(model, notification, parsed); await Send(notification, settings); } @@ -190,12 +198,20 @@ protected override async Task Send(NotificationMessage model, MattermostNotifica { try { - var body = new MattermostBody + var body = new MattermostMessage { - username = string.IsNullOrEmpty(settings.Username) ? "Ombi" : settings.Username, - channel = settings.Channel, - text = model.Message, - icon_url = settings.IconUrl + Username = string.IsNullOrEmpty(settings.Username) ? "Ombi" : settings.Username, + Channel = settings.Channel, + Text = model.Message, + IconUrl = new Uri(settings.IconUrl), + Attachments = new List + { + new MattermostAttachment + { + Title = model.Other.ContainsKey("title") ? model.Other["title"] : string.Empty, + ImageUrl = model.Other.ContainsKey("image") ? new Uri(model.Other["image"]) : null, + } + } }; await Api.PushAsync(settings.WebhookUrl, body); }