diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/218_telegram_link_previewFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/218_telegram_link_previewFixture.cs new file mode 100644 index 000000000..35a0faa2d --- /dev/null +++ b/src/NzbDrone.Core.Test/Datastore/Migration/218_telegram_link_previewFixture.cs @@ -0,0 +1,218 @@ +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration; +using NzbDrone.Core.Notifications.Telegram; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.Datastore.Migration +{ + [TestFixture] + public class telegram_link_previewFixture : MigrationTest + { + [Test] + public void should_set_link_preview_to_none_when_no_metadata_links() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Notifications").Row(new + { + OnGrab = true, + OnDownload = true, + OnUpgrade = true, + OnHealthIssue = true, + IncludeHealthWarnings = true, + OnRename = true, + Name = "Telegram Sonarr", + Implementation = "Telegram", + Tags = "[]", + Settings = new TelegramSettings217 + { + BotToken = "secret", + ChatId = "12345", + SendSilently = false, + IncludeAppNameInTitle = false, + IncludeInstanceNameInTitle = false, + MetadataLinks = new List() + }.ToJson(), + ConfigContract = "TelegramSettings" + }); + }); + + var items = db.Query("SELECT * FROM \"Notifications\""); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Telegram"); + items.First().ConfigContract.Should().Be("TelegramSettings"); + items.First().Settings.LinkPreview.Should().Be((int)MetadataLinkPreviewType.None); + } + + [Test] + public void should_set_link_preview_to_first_metadata_link_when_not_tvdb() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Notifications").Row(new + { + OnGrab = true, + OnDownload = true, + OnUpgrade = true, + OnHealthIssue = true, + IncludeHealthWarnings = true, + OnRename = true, + Name = "Telegram Sonarr", + Implementation = "Telegram", + Tags = "[]", + Settings = new TelegramSettings217 + { + BotToken = "secret", + ChatId = "12345", + SendSilently = false, + IncludeAppNameInTitle = false, + IncludeInstanceNameInTitle = false, + MetadataLinks = new List { 0, 1 } + }.ToJson(), + ConfigContract = "TelegramSettings" + }); + }); + + var items = db.Query("SELECT * FROM \"Notifications\""); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Telegram"); + items.First().ConfigContract.Should().Be("TelegramSettings"); + items.First().Settings.LinkPreview.Should().Be((int)MetadataLinkPreviewType.Imdb); + } + + [Test] + public void should_set_link_preview_to_first_metadata_link_that_is_not_tvdb() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Notifications").Row(new + { + OnGrab = true, + OnDownload = true, + OnUpgrade = true, + OnHealthIssue = true, + IncludeHealthWarnings = true, + OnRename = true, + Name = "Telegram Sonarr", + Implementation = "Telegram", + Tags = "[]", + Settings = new TelegramSettings217 + { + BotToken = "secret", + ChatId = "12345", + SendSilently = false, + IncludeAppNameInTitle = false, + IncludeInstanceNameInTitle = false, + MetadataLinks = new List { 1, 2 } + }.ToJson(), + ConfigContract = "TelegramSettings" + }); + }); + + var items = db.Query("SELECT * FROM \"Notifications\""); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Telegram"); + items.First().ConfigContract.Should().Be("TelegramSettings"); + items.First().Settings.LinkPreview.Should().Be((int)MetadataLinkPreviewType.Tvmaze); + } + + [Test] + public void should_set_link_preview_to_none_when_only_metadata_link_is_tvdb() + { + var db = WithMigrationTestDb(c => + { + c.Insert.IntoTable("Notifications").Row(new + { + OnGrab = true, + OnDownload = true, + OnUpgrade = true, + OnHealthIssue = true, + IncludeHealthWarnings = true, + OnRename = true, + Name = "Telegram Sonarr", + Implementation = "Telegram", + Tags = "[]", + Settings = new TelegramSettings217 + { + BotToken = "secret", + ChatId = "12345", + SendSilently = false, + IncludeAppNameInTitle = false, + IncludeInstanceNameInTitle = false, + MetadataLinks = new List { 1 } + }.ToJson(), + ConfigContract = "TelegramSettings" + }); + }); + + var items = db.Query("SELECT * FROM \"Notifications\""); + + items.Should().HaveCount(1); + items.First().Implementation.Should().Be("Telegram"); + items.First().ConfigContract.Should().Be("TelegramSettings"); + items.First().Settings.LinkPreview.Should().Be((int)MetadataLinkPreviewType.None); + } + } + + public class NotificationDefinition218 + { + public int Id { get; set; } + public string Implementation { get; set; } + public string ConfigContract { get; set; } + public TelegramSettings218 Settings { get; set; } + public string Name { get; set; } + public bool OnGrab { get; set; } + public bool OnDownload { get; set; } + public bool OnUpgrade { get; set; } + public bool OnRename { get; set; } + public bool OnSeriesDelete { get; set; } + public bool OnEpisodeFileDelete { get; set; } + public bool OnEpisodeFileDeleteForUpgrade { get; set; } + public bool OnHealthIssue { get; set; } + public bool OnApplicationUpdate { get; set; } + public bool OnManualInteractionRequired { get; set; } + public bool OnSeriesAdd { get; set; } + public bool OnHealthRestored { get; set; } + public bool OnImportComplete { get; set; } + public bool SupportsOnGrab { get; set; } + public bool SupportsOnDownload { get; set; } + public bool SupportsOnUpgrade { get; set; } + public bool SupportsOnRename { get; set; } + public bool SupportsOnSeriesDelete { get; set; } + public bool SupportsOnEpisodeFileDelete { get; set; } + public bool SupportsOnEpisodeFileDeleteForUpgrade { get; set; } + public bool SupportsOnHealthIssue { get; set; } + public bool IncludeHealthWarnings { get; set; } + public List Tags { get; set; } + } + + public class TelegramSettings217 + { + public string BotToken { get; set; } + public string ChatId { get; set; } + public int? TopicId { get; set; } + public bool SendSilently { get; set; } + public bool IncludeAppNameInTitle { get; set; } + public bool IncludeInstanceNameInTitle { get; set; } + public IEnumerable MetadataLinks { get; set; } + } + + public class TelegramSettings218 + { + public string BotToken { get; set; } + public string ChatId { get; set; } + public int? TopicId { get; set; } + public bool SendSilently { get; set; } + public bool IncludeAppNameInTitle { get; set; } + public bool IncludeInstanceNameInTitle { get; set; } + public IEnumerable MetadataLinks { get; set; } + public int LinkPreview { get; set; } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/218_telegram_link_preview.cs b/src/NzbDrone.Core/Datastore/Migration/218_telegram_link_preview.cs new file mode 100644 index 000000000..4f3572b5f --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/218_telegram_link_preview.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Data; +using System.Linq; +using Dapper; +using FluentMigrator; +using Newtonsoft.Json.Linq; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(218)] + public class telegram_link_preview : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.WithConnection(ChangeEncryption); + } + + private void ChangeEncryption(IDbConnection conn, IDbTransaction tran) + { + var updated = new List(); + using (var cmd = conn.CreateCommand()) + { + cmd.Transaction = tran; + cmd.CommandText = "SELECT \"Id\", \"Settings\" FROM \"Notifications\" WHERE \"Implementation\" = 'Telegram'"; + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var id = reader.GetInt32(0); + var settings = Json.Deserialize(reader.GetString(1)); + + var metadataLinks = settings["metadataLinks"] as JArray; + + if (metadataLinks == null) + { + settings["linkPreview"] = -1; + } + else + { + settings["linkPreview"] = metadataLinks.FirstOrDefault(l => l.Value() != 1) ?? -1; + } + + updated.Add(new + { + Settings = settings.ToJson(), + Id = id + }); + } + } + } + + var updateSql = "UPDATE \"Notifications\" SET \"Settings\" = @Settings WHERE \"Id\" = @Id"; + conn.Execute(updateSql, updated, transaction: tran); + } + } +} diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 4a54ad8fd..6c9cd04b3 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1469,6 +1469,8 @@ "NotificationsTelegramSettingsBotToken": "Bot Token", "NotificationsTelegramSettingsChatId": "Chat ID", "NotificationsTelegramSettingsChatIdHelpText": "You must start a conversation with the bot or add it to your group to receive messages", + "NotificationsTelegramSettingsLinkPreview": "Link Preview", + "NotificationsTelegramSettingsLinkPreviewHelpText": "Determines which link will be previewed in the Telegram notification. Choose 'None' to disable", "NotificationsTelegramSettingsIncludeAppName": "Include {appName} in Title", "NotificationsTelegramSettingsIncludeAppNameHelpText": "Optionally prefix message title with {appName} to differentiate notifications from different applications", "NotificationsTelegramSettingsIncludeInstanceName": "Include Instance Name in Title", diff --git a/src/NzbDrone.Core/Notifications/Telegram/MetadataLinkType.cs b/src/NzbDrone.Core/Notifications/Telegram/MetadataLinkType.cs new file mode 100644 index 000000000..6c10bf80a --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Telegram/MetadataLinkType.cs @@ -0,0 +1,25 @@ +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.Notifications.Telegram +{ + // Maintain the same values as MetadataLinkType + + public enum MetadataLinkPreviewType + { + [FieldOption(Label = "None")] + None = -1, + + [FieldOption(Label = "IMDb")] + Imdb = 0, + + // No preview data is supported for TheTVDB at this time + // [FieldOption(Label = "TVDb")] + // Tvdb = 1, + + [FieldOption(Label = "TVMaze")] + Tvmaze = 2, + + [FieldOption(Label = "Trakt")] + Trakt = 3 + } +} diff --git a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs index b06328a36..17d429f2b 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/Telegram.cs @@ -133,22 +133,22 @@ namespace NzbDrone.Core.Notifications.Telegram if (linkType == MetadataLinkType.Imdb && series.ImdbId.IsNotNullOrWhiteSpace()) { - links.Add(new TelegramLink("IMDb", $"https://www.imdb.com/title/{series.ImdbId}")); + links.Add(new TelegramLink(MetadataLinkType.Imdb, "IMDb", $"https://www.imdb.com/title/{series.ImdbId}")); } if (linkType == MetadataLinkType.Tvdb && series.TvdbId > 0) { - links.Add(new TelegramLink("TVDb", $"http://www.thetvdb.com/?tab=series&id={series.TvdbId}")); + links.Add(new TelegramLink(MetadataLinkType.Tvdb, "TVDb", $"http://www.thetvdb.com/?tab=series&id={series.TvdbId}")); } if (linkType == MetadataLinkType.Trakt && series.TvdbId > 0) { - links.Add(new TelegramLink("Trakt", $"http://trakt.tv/search/tvdb/{series.TvdbId}?id_type=show")); + links.Add(new TelegramLink(MetadataLinkType.Trakt, "Trakt", $"http://trakt.tv/search/tvdb/{series.TvdbId}?id_type=show")); } if (linkType == MetadataLinkType.Tvmaze && series.TvMazeId > 0) { - links.Add(new TelegramLink("TVMaze", $"http://www.tvmaze.com/shows/{series.TvMazeId}/_")); + links.Add(new TelegramLink(MetadataLinkType.Tvmaze, "TVMaze", $"http://www.tvmaze.com/shows/{series.TvMazeId}/_")); } } diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramLink.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramLink.cs index ac131b483..00cfd026e 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramLink.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramLink.cs @@ -2,11 +2,13 @@ namespace NzbDrone.Core.Notifications.Telegram { public class TelegramLink { + public MetadataLinkType? Type { get; set; } public string Label { get; set; } public string Link { get; set; } - public TelegramLink(string label, string link) + public TelegramLink(MetadataLinkType? type, string label, string link) { + Type = type; Label = label; Link = link; } diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramLinkPreviewOptions.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramLinkPreviewOptions.cs new file mode 100644 index 000000000..45b421107 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramLinkPreviewOptions.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace NzbDrone.Core.Notifications.Telegram; + +public class TelegramLinkPreviewOptions +{ + [JsonProperty("is_disabled")] + public bool IsDisabled { get; set; } + + [JsonProperty("url")] + public string Url { get; set; } + + public TelegramLinkPreviewOptions(List links, TelegramSettings settings) + { + IsDisabled = (MetadataLinkPreviewType)settings.LinkPreview == MetadataLinkPreviewType.None; + Url = links.FirstOrDefault(l => l.Type.HasValue && (int)l.Type.Value == settings.LinkPreview)?.Link; + } +} diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramPayload.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramPayload.cs new file mode 100644 index 000000000..bde320dd9 --- /dev/null +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramPayload.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace NzbDrone.Core.Notifications.Telegram; + +public class TelegramPayload +{ + [JsonProperty("chat_id")] + public string ChatId { get; set; } + + [JsonProperty("parse_mode")] + public string ParseMode = "HTML"; + public string Text { get; set; } + + [JsonProperty("disable_notification")] + public bool DisableNotification { get; set; } + + [JsonProperty("message_thread_id")] + public int? MessageThreadId { get; set; } + + [JsonProperty("link_preview_options")] + public TelegramLinkPreviewOptions LinkPreviewOptions { get; set; } +} diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs index bd657e331..c83050992 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramProxy.cs @@ -50,13 +50,22 @@ namespace NzbDrone.Core.Notifications.Telegram var requestBuilder = new HttpRequestBuilder(URL).Resource("bot{token}/sendmessage").Post(); var request = requestBuilder.SetSegment("token", settings.BotToken) - .AddFormParameter("chat_id", settings.ChatId) - .AddFormParameter("parse_mode", "HTML") - .AddFormParameter("text", text) - .AddFormParameter("disable_notification", settings.SendSilently) - .AddFormParameter("message_thread_id", settings.TopicId) + .Accept(HttpAccept.Json) .Build(); + request.Headers.ContentType = "application/json"; + + var payload = new TelegramPayload + { + ChatId = settings.ChatId, + Text = text.ToString(), + DisableNotification = settings.SendSilently, + MessageThreadId = settings.TopicId, + LinkPreviewOptions = new TelegramLinkPreviewOptions(links, settings) + }; + + request.SetContent(payload.ToJson()); + _httpClient.Post(request); } @@ -70,7 +79,7 @@ namespace NzbDrone.Core.Notifications.Telegram var links = new List { - new TelegramLink("Sonarr.tv", "https://sonarr.tv") + new TelegramLink(null, "Sonarr.tv", "https://sonarr.tv") }; var testMessageTitle = settings.IncludeAppNameInTitle ? brandedTitle : title; diff --git a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs index 91bf68cfc..c3a23b85f 100644 --- a/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs +++ b/src/NzbDrone.Core/Notifications/Telegram/TelegramSettings.cs @@ -24,6 +24,20 @@ namespace NzbDrone.Core.Notifications.Telegram } } }); + + RuleFor(c => c.LinkPreview).Custom((link, context) => + { + if (!Enum.IsDefined(typeof(MetadataLinkPreviewType), link)) + { + context.AddFailure("LinkPreview", $"Selected value is not valid: {link}"); + } + }); + + // Ensure the select value is one of the selected metadata links + RuleFor(c => c.LinkPreview) + .Must((model, field) => model.MetadataLinks.Any(link => link == field)) + .Unless(c => c.LinkPreview == (int)MetadataLinkPreviewType.None) + .WithMessage("Link Preview must be one of the selected Metadata Links"); } } @@ -34,6 +48,7 @@ namespace NzbDrone.Core.Notifications.Telegram public TelegramSettings() { MetadataLinks = Enumerable.Empty(); + LinkPreview = (int)MetadataLinkPreviewType.None; } [FieldDefinition(0, Label = "NotificationsTelegramSettingsBotToken", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://core.telegram.org/bots")] @@ -57,6 +72,9 @@ namespace NzbDrone.Core.Notifications.Telegram [FieldDefinition(6, Label = "NotificationsTelegramSettingsMetadataLinks", Type = FieldType.Select, SelectOptions = typeof(MetadataLinkType), HelpText = "NotificationsTelegramSettingsMetadataLinksHelpText")] public IEnumerable MetadataLinks { get; set; } + [FieldDefinition(7, Label = "NotificationsTelegramSettingsLinkPreview", Type = FieldType.Select, SelectOptions = typeof(MetadataLinkPreviewType), HelpText = "NotificationsTelegramSettingsLinkPreviewHelpText")] + public int LinkPreview { get; set; } + public override NzbDroneValidationResult Validate() { return new NzbDroneValidationResult(Validator.Validate(this));