diff --git a/.gitignore b/.gitignore index c41ad2b87..33d37401d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ #Manually added files +src/NadekoBot/data/last_known_version.txt + # medusa stuff !src/NadekoBot/data/medusae/medusa.yml src/NadekoBot/data/medusae/** diff --git a/src/NadekoBot/Common/Configs/BotConfig.cs b/src/NadekoBot/Common/Configs/BotConfig.cs index 059d5d53e..11fe60beb 100644 --- a/src/NadekoBot/Common/Configs/BotConfig.cs +++ b/src/NadekoBot/Common/Configs/BotConfig.cs @@ -12,7 +12,7 @@ namespace NadekoBot.Common.Configs; public sealed partial class BotConfig : ICloneable { [Comment(@"DO NOT CHANGE")] - public int Version { get; set; } = 3; + public int Version { get; set; } = 4; [Comment(@"Most commands, when executed, have a small colored line next to the response. The color depends whether the command @@ -29,12 +29,8 @@ and copy the hex code fo your selected color (marked as #)")] Allowed values: Simple, Normal, None")] public ConsoleOutputType ConsoleOutputType { get; set; } -// [Comment(@"For what kind of updates will the bot check. -// Allowed values: Release, Commit, None")] -// public UpdateCheckType CheckForUpdates { get; set; } - - // [Comment(@"How often will the bot check for updates, in hours")] - // public int CheckUpdateInterval { get; set; } + [Comment(@"Whether the bot will check for new releases every hour")] + public bool CheckForUpdates { get; set; } = true; [Comment(@"Do you want any messages sent by users in Bot's DM to be forwarded to the owner(s)?")] public bool ForwardMessages { get; set; } diff --git a/src/NadekoBot/Modules/Administration/Self/CheckForUpdatesService.cs b/src/NadekoBot/Modules/Administration/Self/CheckForUpdatesService.cs new file mode 100644 index 000000000..e0622bd1e --- /dev/null +++ b/src/NadekoBot/Modules/Administration/Self/CheckForUpdatesService.cs @@ -0,0 +1,153 @@ +using System.Net.Http.Json; +using System.Text; +using NadekoBot.Common.ModuleBehaviors; + +namespace NadekoBot.Modules.Administration.Self; + +public sealed class CheckForUpdatesService : INService, IReadyExecutor +{ + private readonly BotConfigService _bcs; + private readonly IBotCredsProvider _bcp; + private readonly IHttpClientFactory _httpFactory; + private readonly DiscordSocketClient _client; + private readonly IEmbedBuilderService _ebs; + + public CheckForUpdatesService(BotConfigService bcs, IBotCredsProvider bcp, IHttpClientFactory httpFactory, + DiscordSocketClient client, IEmbedBuilderService ebs) + { + _bcs = bcs; + _bcp = bcp; + _httpFactory = httpFactory; + _client = client; + _ebs = ebs; + } + + public async Task OnReadyAsync() + { + if (_client.ShardId != 0) + return; + + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(15)); + while (await timer.WaitForNextTickAsync()) + { + var conf = _bcs.Data; + + if (!conf.CheckForUpdates) + continue; + + try + { + const string URL = "https://cdn.nadeko.bot/cmds/versions.json"; + using var http = _httpFactory.CreateClient(); + var versions = await http.GetFromJsonAsync(URL); + + if (versions is null) + continue; + + var latest = versions[0]; + var latestVersion = Version.Parse(latest); + var lastKnownVersion = GetLastKnownVersion(); + + if (lastKnownVersion is null) + { + UpdateLastKnownVersion(latestVersion); + continue; + } + + if (latestVersion > lastKnownVersion) + { + UpdateLastKnownVersion(latestVersion); + + // pull changelog + var changelog = await http.GetStringAsync("https://gitlab.com/Kwoth/nadekobot/-/raw/v4/CHANGELOG.md"); + + var thisVersionChangelog = GetVersionChangelog(latestVersion, changelog); + + if (string.IsNullOrWhiteSpace(thisVersionChangelog)) + { + Log.Warning("New version {BotVersion} was found but changelog is unavailable", + thisVersionChangelog); + continue; + } + + var creds = _bcp.GetCreds(); + await creds.OwnerIds + .Select(async x => + { + var user = await _client.GetUserAsync(x); + if (user is null) + return; + + var eb = _ebs.Create() + .WithOkColor() + .WithAuthor($"NadekoBot v{latestVersion} Released!") + .WithTitle("Changelog") + .WithUrl("https://gitlab.com/Kwoth/nadekobot/-/blob/v4/CHANGELOG.md") + .WithDescription(thisVersionChangelog.TrimTo(4096)) + .WithFooter("You may disable these messages by typing '.conf bot checkforupdates false'"); + + await user.EmbedAsync(eb); + }).WhenAll(); + } + } + catch (Exception ex) + { + Log.Error(ex, "Error while checking for new bot release: {ErrorMessage}", ex.Message); + } + } + } + + private string? GetVersionChangelog(Version latestVersion, string changelog) + { + var clSpan = changelog.AsSpan(); + + var sb = new StringBuilder(); + var started = false; + foreach (var line in clSpan.EnumerateLines()) + { + // if we're at the current version, keep reading lines and adding to the output + if (started) + { + // if we got to previous version, end + if (line.StartsWith("## [")) + break; + + // if we're reading a new segment, reformat it to print it better to discord + if (line.StartsWith("### ")) + { + sb.AppendLine(Format.Bold(line.ToString())); + } + else + { + sb.AppendLine(line.ToString()); + } + + continue; + } + + if (line.StartsWith($"## [{latestVersion.ToString()}]")) + { + started = true; + continue; + } + } + + return sb.ToString(); + } + + private const string LAST_KNOWN_VERSION_PATH = "data/last_known_version.txt"; + private Version? GetLastKnownVersion() + { + if (!File.Exists(LAST_KNOWN_VERSION_PATH)) + return null; + + return Version.TryParse(File.ReadAllText(LAST_KNOWN_VERSION_PATH), out var ver) + ? ver + : null; + } + + private void UpdateLastKnownVersion(Version version) + { + File.WriteAllText("data/last_known_version.txt", version.ToString()); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Services/Settings/BotConfigService.cs b/src/NadekoBot/Services/Settings/BotConfigService.cs index 336f51458..2644db8f0 100644 --- a/src/NadekoBot/Services/Settings/BotConfigService.cs +++ b/src/NadekoBot/Services/Settings/BotConfigService.cs @@ -24,6 +24,7 @@ public sealed class BotConfigService : ConfigServiceBase AddParsedProp("console.type", bs => bs.ConsoleOutputType, Enum.TryParse, ConfigPrinters.ToString); AddParsedProp("locale", bs => bs.DefaultLocale, ConfigParsers.Culture, ConfigPrinters.Culture); AddParsedProp("prefix", bs => bs.Prefix, ConfigParsers.String, ConfigPrinters.ToString); + AddParsedProp("checkforupdates", bs => bs.CheckForUpdates, bool.TryParse, ConfigPrinters.ToString); Migrate(); } @@ -48,5 +49,12 @@ public sealed class BotConfigService : ConfigServiceBase .ToHashSet(); }); } + + if (data.Version < 4) + ModifyConfig(c => + { + c.Version = 4; + c.CheckForUpdates = true; + }); } } \ No newline at end of file