Update Notification Refactor (#511)

* Replaced profile links to anchors so we can open in new tab if we like

* Refactored how update checking works. We now explicitly check and send back on the same API. We have a weekly job that will push an update to the user.

* Implemented a changelog tab

* Ported over a GA fix for using ' in PR bodies.

* Don't check cert for Github
This commit is contained in:
Joseph Milazzo 2021-08-19 16:49:53 -07:00 committed by GitHub
parent 0e48aeebc5
commit 2a76092566
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 246 additions and 56 deletions

View file

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using API.DTOs.Stats;
using API.DTOs.Update;
using API.Extensions;
using API.Interfaces;
using API.Interfaces.Services;
@ -25,9 +27,11 @@ namespace API.Controllers
private readonly IArchiveService _archiveService;
private readonly ICacheService _cacheService;
private readonly ITaskScheduler _taskScheduler;
private readonly IVersionUpdaterService _versionUpdaterService;
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger, IConfiguration config,
IBackupService backupService, IArchiveService archiveService, ICacheService cacheService, ITaskScheduler taskScheduler)
IBackupService backupService, IArchiveService archiveService, ICacheService cacheService, ITaskScheduler taskScheduler,
IVersionUpdaterService versionUpdaterService)
{
_applicationLifetime = applicationLifetime;
_logger = logger;
@ -36,6 +40,7 @@ namespace API.Controllers
_archiveService = archiveService;
_cacheService = cacheService;
_taskScheduler = taskScheduler;
_versionUpdaterService = versionUpdaterService;
}
/// <summary>
@ -102,11 +107,16 @@ namespace API.Controllers
}
}
[HttpPost("check-update")]
public ActionResult CheckForUpdates()
[HttpGet("check-update")]
public async Task<ActionResult<UpdateNotificationDto>> CheckForUpdates()
{
_taskScheduler.CheckForUpdate();
return Ok();
return Ok(await _versionUpdaterService.CheckForUpdate());
}
[HttpGet("changelog")]
public async Task<ActionResult<IEnumerable<UpdateNotificationDto>>> GetChangelog()
{
return Ok(await _versionUpdaterService.GetAllReleases());
}
}
}

View file

@ -0,0 +1,38 @@
namespace API.DTOs.Update
{
/// <summary>
/// Update Notification denoting a new release available for user to update to
/// </summary>
public class UpdateNotificationDto
{
/// <summary>
/// Current installed Version
/// </summary>
public string CurrentVersion { get; init; }
/// <summary>
/// Semver of the release version
/// <example>0.4.3</example>
/// </summary>
public string UpdateVersion { get; init; }
/// <summary>
/// Release body in HTML
/// </summary>
public string UpdateBody { get; init; }
/// <summary>
/// Title of the release
/// </summary>
public string UpdateTitle { get; init; }
/// <summary>
/// Github Url
/// </summary>
public string UpdateUrl { get; init; }
/// <summary>
/// If this install is within Docker
/// </summary>
public bool IsDocker { get; init; }
/// <summary>
/// Is this a pre-release
/// </summary>
public bool IsPrerelease { get; init; }
}
}

View file

@ -15,6 +15,5 @@
void RefreshSeriesMetadata(int libraryId, int seriesId);
void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false);
void CancelStatsTasks();
void CheckForUpdate();
}
}

View file

@ -1,11 +1,15 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using API.DTOs.Update;
using API.Services.Tasks;
namespace API.Interfaces.Services
{
public interface IVersionUpdaterService
{
public Task CheckForUpdate();
Task<UpdateNotificationDto> CheckForUpdate();
Task PushUpdate(UpdateNotificationDto update);
Task<IEnumerable<UpdateNotificationDto>> GetAllReleases();
}
}

View file

@ -70,6 +70,8 @@ namespace API.Services
}
RecurringJob.AddOrUpdate("cleanup", () => _cleanupService.Cleanup(), Cron.Daily);
RecurringJob.AddOrUpdate("check-for-updates", () => _scannerService.ScanLibraries(), Cron.Daily);
}
#region StatsTasks
@ -104,7 +106,7 @@ namespace API.Services
public void ScheduleUpdaterTasks()
{
_logger.LogInformation("Scheduling Auto-Update tasks");
RecurringJob.AddOrUpdate("check-updates", () => _versionUpdaterService.CheckForUpdate(), Cron.Daily);
RecurringJob.AddOrUpdate("check-updates", () => CheckForUpdate(), Cron.Weekly);
}
#endregion
@ -152,9 +154,13 @@ namespace API.Services
BackgroundJob.Enqueue(() => _backupService.BackupDatabase());
}
public void CheckForUpdate()
/// <summary>
/// Not an external call. Only public so that we can call this for a Task
/// </summary>
public async Task CheckForUpdate()
{
BackgroundJob.Enqueue(() => _versionUpdaterService.CheckForUpdate());
var update = await _versionUpdaterService.CheckForUpdate();
await _versionUpdaterService.PushUpdate(update);
}
}
}

View file

@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using API.DTOs.Update;
using API.Interfaces.Services;
using API.SignalR;
using API.SignalR.Presence;
using Flurl.Http;
using Flurl.Http.Configuration;
using Kavita.Common.EnvironmentInfo;
using MarkdownDeep;
using Microsoft.AspNetCore.SignalR;
@ -32,36 +36,81 @@ namespace API.Services.Tasks
/// Url of the release on Github
/// </summary>
public string Html_Url { get; init; }
}
public class UntrustedCertClientFactory : DefaultHttpClientFactory
{
public override HttpMessageHandler CreateMessageHandler() {
return new HttpClientHandler {
ServerCertificateCustomValidationCallback = (a, b, c, d) => true
};
}
}
public class VersionUpdaterService : IVersionUpdaterService
{
private readonly ILogger<VersionUpdaterService> _logger;
private readonly IHubContext<MessageHub> _messageHub;
private readonly IPresenceTracker _tracker;
private readonly Markdown _markdown = new MarkdownDeep.Markdown();
private static readonly string GithubLatestReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases/latest";
private static readonly string GithubAllReleasesUrl = "https://api.github.com/repos/Kareadita/Kavita/releases";
public VersionUpdaterService(ILogger<VersionUpdaterService> logger, IHubContext<MessageHub> messageHub, IPresenceTracker tracker)
{
_logger = logger;
_messageHub = messageHub;
_tracker = tracker;
FlurlHttp.ConfigureClient(GithubLatestReleasesUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
FlurlHttp.ConfigureClient(GithubAllReleasesUrl, cli =>
cli.Settings.HttpClientFactory = new UntrustedCertClientFactory());
}
/// <summary>
/// Scheduled Task that checks if a newer version is available. If it is, will check if User is currently connected and push
/// a message.
/// Fetches the latest release from Github
/// </summary>
public async Task CheckForUpdate()
public async Task<UpdateNotificationDto> CheckForUpdate()
{
var update = await GetGithubRelease();
return CreateDto(update);
}
if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return;
/// <summary>
///
/// </summary>
/// <returns></returns>
public async Task<IEnumerable<UpdateNotificationDto>> GetAllReleases()
{
var updates = await GetGithubReleases();
return updates.Select(CreateDto);
}
var admins = await _tracker.GetOnlineAdmins();
private UpdateNotificationDto? CreateDto(GithubReleaseMetadata update)
{
if (update == null || string.IsNullOrEmpty(update.Tag_Name)) return null;
var version = update.Tag_Name.Replace("v", string.Empty);
var updateVersion = new Version(version);
return new UpdateNotificationDto()
{
CurrentVersion = version,
UpdateVersion = updateVersion.ToString(),
UpdateBody = _markdown.Transform(update.Body.Trim()),
UpdateTitle = update.Name,
UpdateUrl = update.Html_Url,
IsDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker
};
}
public async Task PushUpdate(UpdateNotificationDto update)
{
if (update == null) return;
var admins = await _tracker.GetOnlineAdmins();
var updateVersion = new Version(update.CurrentVersion);
if (BuildInfo.Version < updateVersion)
{
_logger.LogInformation("Server is out of date. Current: {CurrentVersion}. Available: {AvailableUpdate}", BuildInfo.Version, updateVersion);
@ -74,10 +123,8 @@ namespace API.Services.Tasks
}
}
private async Task SendEvent(GithubReleaseMetadata update, IReadOnlyList<string> admins)
private async Task SendEvent(UpdateNotificationDto update, IReadOnlyList<string> admins)
{
var version = update.Tag_Name.Replace("v", string.Empty);
var updateVersion = new Version(version);
var connections = new List<string>();
foreach (var admin in admins)
{
@ -87,26 +134,29 @@ namespace API.Services.Tasks
await _messageHub.Clients.Users(admins).SendAsync("UpdateAvailable", new SignalRMessage
{
Name = "UpdateAvailable",
Body = new
{
CurrentVersion = version,
UpdateVersion = updateVersion.ToString(),
UpdateBody = _markdown.Transform(update.Body.Trim()),
UpdateTitle = update.Name,
UpdateUrl = update.Html_Url,
IsDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker
}
Body = update
});
}
private static async Task<GithubReleaseMetadata> GetGithubRelease()
{
var update = await "https://api.github.com/repos/Kareadita/Kavita/releases/latest"
var update = await GithubLatestReleasesUrl
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.GetJsonAsync<GithubReleaseMetadata>();
return update;
}
private static async Task<IEnumerable<GithubReleaseMetadata>> GetGithubReleases()
{
var update = await GithubAllReleasesUrl
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.GetJsonAsync<IEnumerable<GithubReleaseMetadata>>();
return update;
}
}
}

View file

@ -7,27 +7,30 @@ using Microsoft.AspNetCore.SignalR;
namespace API.SignalR
{
/// <summary>
/// Generic hub for sending messages to UI
/// </summary>
[Authorize]
public class MessageHub : Hub
{
private static readonly HashSet<string> _connections = new HashSet<string>();
private static readonly HashSet<string> Connections = new HashSet<string>();
public static bool IsConnected
{
get
{
lock (_connections)
lock (Connections)
{
return _connections.Count != 0;
return Connections.Count != 0;
}
}
}
public override async Task OnConnectedAsync()
{
lock (_connections)
lock (Connections)
{
_connections.Add(Context.ConnectionId);
Connections.Add(Context.ConnectionId);
}
await base.OnConnectedAsync();
@ -35,9 +38,9 @@ namespace API.SignalR
public override async Task OnDisconnectedAsync(Exception exception)
{
lock (_connections)
lock (Connections)
{
_connections.Remove(Context.ConnectionId);
Connections.Remove(Context.ConnectionId);
}
await base.OnDisconnectedAsync(exception);

View file

@ -6,6 +6,9 @@ using Microsoft.AspNetCore.SignalR;
namespace API.SignalR
{
/// <summary>
/// Keeps track of who is logged into the app
/// </summary>
public class PresenceHub : Hub
{
private readonly IPresenceTracker _tracker;