Logging Enhancements (#1521)

* Recreated Kavita Logging with Serilog instead of Default. This needs to be move out of the appsettings now, to allow auto updater to patch.

* Refactored the code to be completely configured via Code rather than appsettings.json. This is a required step for Auto Updating.

* Added in the ability to send logs directly to the UI only for users on the log route. Stopping implementation as Alerts page will handle the rest of the implementation.

* Fixed up the backup service to not rely on Config from appsettings.json

* Tweaked the Logging levels available

* Moved everything over to File-scoped namespaces

* Moved everything over to File-scoped namespaces

* Code cleanup, removed an old migration and changed so debug logging doesn't print sensitive db data

* Removed dead code
This commit is contained in:
Joseph Milazzo 2022-09-12 19:25:48 -05:00 committed by GitHub
parent 9f715cc35f
commit d1a14f7e68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
212 changed files with 16599 additions and 16834 deletions

View file

@ -56,4 +56,5 @@ public class EventHub : IEventHub
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
await _messageHub.Clients.User(user.UserName).SendAsync(method, message);
}
}

58
API/SignalR/LogHub.cs Normal file
View file

@ -0,0 +1,58 @@
using System;
using System.Threading.Tasks;
using API.Extensions;
using API.SignalR.Presence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace API.SignalR;
public interface ILogHub : Serilog.Sinks.AspNetCore.SignalR.Interfaces.IHub
{
}
[Authorize]
public class LogHub : Hub<ILogHub>
{
private readonly IEventHub _eventHub;
private readonly IPresenceTracker _tracker;
public LogHub(IEventHub eventHub, IPresenceTracker tracker)
{
_eventHub = eventHub;
_tracker = tracker;
}
public override async Task OnConnectedAsync()
{
await _tracker.UserConnected(Context.User.GetUsername(), Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await _tracker.UserDisconnected(Context.User.GetUsername(), Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
public async Task SendLogAsString(string message)
{
await _eventHub.SendMessageAsync("LogString", new SignalRMessage()
{
Body = message,
EventType = "LogString",
Name = "LogString",
}, true);
}
public async Task SendLogAsObject(object messageObject)
{
await _eventHub.SendMessageAsync("LogObject", new SignalRMessage()
{
Body = messageObject,
EventType = "LogString",
Name = "LogString",
}, true);
}
}

View file

@ -6,476 +6,475 @@ using API.DTOs.Update;
using API.Entities;
using API.Extensions;
namespace API.SignalR
namespace API.SignalR;
public static class MessageFactoryEntityTypes
{
public static class MessageFactoryEntityTypes
public const string Series = "series";
public const string Volume = "volume";
public const string Chapter = "chapter";
public const string CollectionTag = "collection";
public const string ReadingList = "readingList";
}
public static class MessageFactory
{
/// <summary>
/// An update is available for the Kavita instance
/// </summary>
public const string UpdateAvailable = "UpdateAvailable";
/// <summary>
/// Used to tell when a scan series completes. This also informs UI to update series metadata
/// </summary>
public const string ScanSeries = "ScanSeries";
/// <summary>
/// Event sent out during Refresh Metadata for progress tracking
/// </summary>
private const string CoverUpdateProgress = "CoverUpdateProgress";
/// <summary>
/// Series is added to server
/// </summary>
public const string SeriesAdded = "SeriesAdded";
/// <summary>
/// Series is removed from server
/// </summary>
public const string SeriesRemoved = "SeriesRemoved";
/// <summary>
/// When a user is connects/disconnects from server
/// </summary>
public const string OnlineUsers = "OnlineUsers";
/// <summary>
/// When a series is added to a collection
/// </summary>
public const string SeriesAddedToCollection = "SeriesAddedToCollection";
/// <summary>
/// Event sent out during backing up the database
/// </summary>
private const string BackupDatabaseProgress = "BackupDatabaseProgress";
/// <summary>
/// Event sent out during cleaning up temp and cache folders
/// </summary>
private const string CleanupProgress = "CleanupProgress";
/// <summary>
/// Event sent out during downloading of files
/// </summary>
private const string DownloadProgress = "DownloadProgress";
/// <summary>
/// A cover was updated
/// </summary>
public const string CoverUpdate = "CoverUpdate";
/// <summary>
/// A custom site theme was removed or added
/// </summary>
private const string SiteThemeProgress = "SiteThemeProgress";
/// <summary>
/// A custom book theme was removed or added
/// </summary>
private const string BookThemeProgress = "BookThemeProgress";
/// <summary>
/// A type of event that has progress (determinate or indeterminate).
/// The underlying event will have a name to give details on how to handle.
/// </summary>
/// <remarks>This is not an Event Name, it is used as the method only</remarks>
public const string NotificationProgress = "NotificationProgress";
/// <summary>
/// Event sent out when Scan Loop is parsing a file
/// </summary>
private const string FileScanProgress = "FileScanProgress";
/// <summary>
/// A generic error that can occur in background processing
/// </summary>
public const string Error = "Error";
/// <summary>
/// When DB updates are occuring during a library/series scan
/// </summary>
private const string ScanProgress = "ScanProgress";
/// <summary>
/// When a library is created/deleted in the Server
/// </summary>
public const string LibraryModified = "LibraryModified";
/// <summary>
/// A user's progress was modified
/// </summary>
public const string UserProgressUpdate = "UserProgressUpdate";
/// <summary>
/// A user's account or preferences were updated and UI needs to refresh to stay in sync
/// </summary>
public const string UserUpdate = "UserUpdate";
/// <summary>
/// When bulk bookmarks are being converted
/// </summary>
private const string ConvertBookmarksProgress = "ConvertBookmarksProgress";
/// <summary>
/// When files are being scanned to calculate word count
/// </summary>
private const string WordCountAnalyzerProgress = "WordCountAnalyzerProgress";
/// <summary>
/// A generic message that can occur in background processing to inform user, but no direct action is needed
/// </summary>
public const string Info = "Info";
public static SignalRMessage ScanSeriesEvent(int libraryId, int seriesId, string seriesName)
{
public const string Series = "series";
public const string Volume = "volume";
public const string Chapter = "chapter";
public const string CollectionTag = "collection";
public const string ReadingList = "readingList";
return new SignalRMessage()
{
Name = ScanSeries,
EventType = ProgressEventType.Single,
Body = new
{
LibraryId = libraryId,
SeriesId = seriesId,
SeriesName = seriesName
}
};
}
public static class MessageFactory
public static SignalRMessage SeriesAddedEvent(int seriesId, string seriesName, int libraryId)
{
/// <summary>
/// An update is available for the Kavita instance
/// </summary>
public const string UpdateAvailable = "UpdateAvailable";
/// <summary>
/// Used to tell when a scan series completes. This also informs UI to update series metadata
/// </summary>
public const string ScanSeries = "ScanSeries";
/// <summary>
/// Event sent out during Refresh Metadata for progress tracking
/// </summary>
private const string CoverUpdateProgress = "CoverUpdateProgress";
/// <summary>
/// Series is added to server
/// </summary>
public const string SeriesAdded = "SeriesAdded";
/// <summary>
/// Series is removed from server
/// </summary>
public const string SeriesRemoved = "SeriesRemoved";
/// <summary>
/// When a user is connects/disconnects from server
/// </summary>
public const string OnlineUsers = "OnlineUsers";
/// <summary>
/// When a series is added to a collection
/// </summary>
public const string SeriesAddedToCollection = "SeriesAddedToCollection";
/// <summary>
/// Event sent out during backing up the database
/// </summary>
private const string BackupDatabaseProgress = "BackupDatabaseProgress";
/// <summary>
/// Event sent out during cleaning up temp and cache folders
/// </summary>
private const string CleanupProgress = "CleanupProgress";
/// <summary>
/// Event sent out during downloading of files
/// </summary>
private const string DownloadProgress = "DownloadProgress";
/// <summary>
/// A cover was updated
/// </summary>
public const string CoverUpdate = "CoverUpdate";
/// <summary>
/// A custom site theme was removed or added
/// </summary>
private const string SiteThemeProgress = "SiteThemeProgress";
/// <summary>
/// A custom book theme was removed or added
/// </summary>
private const string BookThemeProgress = "BookThemeProgress";
/// <summary>
/// A type of event that has progress (determinate or indeterminate).
/// The underlying event will have a name to give details on how to handle.
/// </summary>
/// <remarks>This is not an Event Name, it is used as the method only</remarks>
public const string NotificationProgress = "NotificationProgress";
/// <summary>
/// Event sent out when Scan Loop is parsing a file
/// </summary>
private const string FileScanProgress = "FileScanProgress";
/// <summary>
/// A generic error that can occur in background processing
/// </summary>
public const string Error = "Error";
/// <summary>
/// When DB updates are occuring during a library/series scan
/// </summary>
private const string ScanProgress = "ScanProgress";
/// <summary>
/// When a library is created/deleted in the Server
/// </summary>
public const string LibraryModified = "LibraryModified";
/// <summary>
/// A user's progress was modified
/// </summary>
public const string UserProgressUpdate = "UserProgressUpdate";
/// <summary>
/// A user's account or preferences were updated and UI needs to refresh to stay in sync
/// </summary>
public const string UserUpdate = "UserUpdate";
/// <summary>
/// When bulk bookmarks are being converted
/// </summary>
private const string ConvertBookmarksProgress = "ConvertBookmarksProgress";
/// <summary>
/// When files are being scanned to calculate word count
/// </summary>
private const string WordCountAnalyzerProgress = "WordCountAnalyzerProgress";
/// <summary>
/// A generic message that can occur in background processing to inform user, but no direct action is needed
/// </summary>
public const string Info = "Info";
public static SignalRMessage ScanSeriesEvent(int libraryId, int seriesId, string seriesName)
return new SignalRMessage()
{
return new SignalRMessage()
Name = SeriesAdded,
Body = new
{
Name = ScanSeries,
EventType = ProgressEventType.Single,
Body = new
{
LibraryId = libraryId,
SeriesId = seriesId,
SeriesName = seriesName
}
};
}
SeriesId = seriesId,
SeriesName = seriesName,
LibraryId = libraryId
}
};
}
public static SignalRMessage SeriesAddedEvent(int seriesId, string seriesName, int libraryId)
public static SignalRMessage SeriesRemovedEvent(int seriesId, string seriesName, int libraryId)
{
return new SignalRMessage()
{
return new SignalRMessage()
Name = SeriesRemoved,
Body = new
{
Name = SeriesAdded,
Body = new
{
SeriesId = seriesId,
SeriesName = seriesName,
LibraryId = libraryId
}
};
}
SeriesId = seriesId,
SeriesName = seriesName,
LibraryId = libraryId
}
};
}
public static SignalRMessage SeriesRemovedEvent(int seriesId, string seriesName, int libraryId)
public static SignalRMessage WordCountAnalyzerProgressEvent(int libraryId, float progress, string eventType, string subtitle = "")
{
return new SignalRMessage()
{
return new SignalRMessage()
Name = WordCountAnalyzerProgress,
Title = "Analyzing Word count",
SubTitle = subtitle,
EventType = eventType,
Progress = ProgressType.Determinate,
Body = new
{
Name = SeriesRemoved,
Body = new
{
SeriesId = seriesId,
SeriesName = seriesName,
LibraryId = libraryId
}
};
}
LibraryId = libraryId,
Progress = progress,
EventTime = DateTime.Now
}
};
}
public static SignalRMessage WordCountAnalyzerProgressEvent(int libraryId, float progress, string eventType, string subtitle = "")
public static SignalRMessage CoverUpdateProgressEvent(int libraryId, float progress, string eventType, string subtitle = "")
{
return new SignalRMessage()
{
return new SignalRMessage()
Name = CoverUpdateProgress,
Title = "Refreshing Covers",
SubTitle = subtitle,
EventType = eventType,
Progress = ProgressType.Determinate,
Body = new
{
Name = WordCountAnalyzerProgress,
Title = "Analyzing Word count",
SubTitle = subtitle,
EventType = eventType,
Progress = ProgressType.Determinate,
Body = new
{
LibraryId = libraryId,
Progress = progress,
EventTime = DateTime.Now
}
};
}
LibraryId = libraryId,
Progress = progress,
EventTime = DateTime.Now
}
};
}
public static SignalRMessage CoverUpdateProgressEvent(int libraryId, float progress, string eventType, string subtitle = "")
public static SignalRMessage BackupDatabaseProgressEvent(float progress, string subtitle = "")
{
return new SignalRMessage()
{
return new SignalRMessage()
Name = BackupDatabaseProgress,
Title = "Backing up Database",
SubTitle = subtitle,
EventType = progress switch
{
Name = CoverUpdateProgress,
Title = "Refreshing Covers",
SubTitle = subtitle,
EventType = eventType,
Progress = ProgressType.Determinate,
Body = new
{
LibraryId = libraryId,
Progress = progress,
EventTime = DateTime.Now
}
};
}
public static SignalRMessage BackupDatabaseProgressEvent(float progress, string subtitle = "")
0f => "started",
1f => "ended",
_ => "updated"
},
Progress = ProgressType.Determinate,
Body = new
{
Progress = progress
}
};
}
public static SignalRMessage CleanupProgressEvent(float progress, string subtitle = "")
{
return new SignalRMessage()
{
return new SignalRMessage()
Name = CleanupProgress,
Title = "Performing Cleanup",
SubTitle = subtitle,
EventType = progress switch
{
Name = BackupDatabaseProgress,
Title = "Backing up Database",
SubTitle = subtitle,
EventType = progress switch
{
0f => "started",
1f => "ended",
_ => "updated"
},
Progress = ProgressType.Determinate,
Body = new
{
Progress = progress
}
};
}
public static SignalRMessage CleanupProgressEvent(float progress, string subtitle = "")
0f => "started",
1f => "ended",
_ => "updated"
},
Progress = ProgressType.Determinate,
Body = new
{
Progress = progress
}
};
}
public static SignalRMessage UpdateVersionEvent(UpdateNotificationDto update)
{
return new SignalRMessage
{
return new SignalRMessage()
{
Name = CleanupProgress,
Title = "Performing Cleanup",
SubTitle = subtitle,
EventType = progress switch
{
0f => "started",
1f => "ended",
_ => "updated"
},
Progress = ProgressType.Determinate,
Body = new
{
Progress = progress
}
};
}
Name = UpdateAvailable,
Title = "Update Available",
SubTitle = update.UpdateTitle,
EventType = ProgressEventType.Single,
Progress = ProgressType.None,
Body = update
};
}
public static SignalRMessage UpdateVersionEvent(UpdateNotificationDto update)
public static SignalRMessage SeriesAddedToCollectionEvent(int tagId, int seriesId)
{
return new SignalRMessage
{
return new SignalRMessage
Name = SeriesAddedToCollection,
Progress = ProgressType.None,
EventType = ProgressEventType.Single,
Body = new
{
Name = UpdateAvailable,
Title = "Update Available",
SubTitle = update.UpdateTitle,
EventType = ProgressEventType.Single,
Progress = ProgressType.None,
Body = update
};
}
TagId = tagId,
SeriesId = seriesId
}
};
}
public static SignalRMessage SeriesAddedToCollectionEvent(int tagId, int seriesId)
public static SignalRMessage ErrorEvent(string title, string subtitle)
{
return new SignalRMessage
{
return new SignalRMessage
Name = Error,
Title = title,
SubTitle = subtitle,
Progress = ProgressType.None,
EventType = ProgressEventType.Single,
Body = new
{
Name = SeriesAddedToCollection,
Progress = ProgressType.None,
EventType = ProgressEventType.Single,
Body = new
{
TagId = tagId,
SeriesId = seriesId
}
};
}
public static SignalRMessage ErrorEvent(string title, string subtitle)
{
return new SignalRMessage
{
Name = Error,
Title = title,
SubTitle = subtitle,
Progress = ProgressType.None,
EventType = ProgressEventType.Single,
Body = new
{
Title = title,
SubTitle = subtitle,
}
};
}
}
};
}
public static SignalRMessage InfoEvent(string title, string subtitle)
public static SignalRMessage InfoEvent(string title, string subtitle)
{
return new SignalRMessage
{
return new SignalRMessage
Name = Info,
Title = title,
SubTitle = subtitle,
Progress = ProgressType.None,
EventType = ProgressEventType.Single,
Body = new
{
Name = Info,
Title = title,
SubTitle = subtitle,
Progress = ProgressType.None,
EventType = ProgressEventType.Single,
Body = new
{
Title = title,
SubTitle = subtitle,
}
};
}
}
};
}
public static SignalRMessage LibraryModifiedEvent(int libraryId, string action)
public static SignalRMessage LibraryModifiedEvent(int libraryId, string action)
{
return new SignalRMessage
{
return new SignalRMessage
Name = LibraryModified,
Title = "Library modified",
Progress = ProgressType.None,
EventType = ProgressEventType.Single,
Body = new
{
Name = LibraryModified,
Title = "Library modified",
Progress = ProgressType.None,
EventType = ProgressEventType.Single,
Body = new
{
LibrayId = libraryId,
Action = action,
}
};
}
LibrayId = libraryId,
Action = action,
}
};
}
public static SignalRMessage DownloadProgressEvent(string username, string downloadName, float progress, string eventType = "updated")
public static SignalRMessage DownloadProgressEvent(string username, string downloadName, float progress, string eventType = "updated")
{
return new SignalRMessage()
{
return new SignalRMessage()
Name = DownloadProgress,
Title = $"Downloading {downloadName}",
SubTitle = $"Preparing {username.SentenceCase()} the download of {downloadName}",
EventType = eventType,
Progress = ProgressType.Determinate,
Body = new
{
Name = DownloadProgress,
Title = $"Downloading {downloadName}",
SubTitle = $"Preparing {username.SentenceCase()} the download of {downloadName}",
EventType = eventType,
Progress = ProgressType.Determinate,
Body = new
{
UserName = username,
DownloadName = downloadName,
Progress = progress
}
};
}
UserName = username,
DownloadName = downloadName,
Progress = progress
}
};
}
/// <summary>
/// Represents a file being scanned by Kavita for processing and grouping
/// </summary>
/// <remarks>Does not have a progress as it's unknown how many files there are. Instead sends -1 to represent indeterminate</remarks>
/// <param name="folderPath"></param>
/// <param name="libraryName"></param>
/// <param name="eventType"></param>
/// <returns></returns>
public static SignalRMessage FileScanProgressEvent(string folderPath, string libraryName, string eventType)
/// <summary>
/// Represents a file being scanned by Kavita for processing and grouping
/// </summary>
/// <remarks>Does not have a progress as it's unknown how many files there are. Instead sends -1 to represent indeterminate</remarks>
/// <param name="folderPath"></param>
/// <param name="libraryName"></param>
/// <param name="eventType"></param>
/// <returns></returns>
public static SignalRMessage FileScanProgressEvent(string folderPath, string libraryName, string eventType)
{
return new SignalRMessage()
{
return new SignalRMessage()
Name = FileScanProgress,
Title = $"Scanning {libraryName}",
SubTitle = folderPath,
EventType = eventType,
Progress = ProgressType.Indeterminate,
Body = new
{
Name = FileScanProgress,
Title = $"Scanning {libraryName}",
SubTitle = folderPath,
EventType = eventType,
Progress = ProgressType.Indeterminate,
Body = new
{
Title = $"Scanning {libraryName}",
Subtitle = folderPath,
Filename = folderPath,
EventTime = DateTime.Now,
}
};
}
Subtitle = folderPath,
Filename = folderPath,
EventTime = DateTime.Now,
}
};
}
/// <summary>
/// This informs the UI with details about what is being processed by the Scanner
/// </summary>
/// <param name="libraryName"></param>
/// <param name="eventType"></param>
/// <param name="seriesName"></param>
/// <returns></returns>
public static SignalRMessage LibraryScanProgressEvent(string libraryName, string eventType, string seriesName = "")
/// <summary>
/// This informs the UI with details about what is being processed by the Scanner
/// </summary>
/// <param name="libraryName"></param>
/// <param name="eventType"></param>
/// <param name="seriesName"></param>
/// <returns></returns>
public static SignalRMessage LibraryScanProgressEvent(string libraryName, string eventType, string seriesName = "")
{
return new SignalRMessage()
{
return new SignalRMessage()
{
Name = ScanProgress,
Title = $"Processing {seriesName}",
SubTitle = seriesName,
EventType = eventType,
Progress = ProgressType.Indeterminate,
Body = null
};
}
Name = ScanProgress,
Title = $"Processing {seriesName}",
SubTitle = seriesName,
EventType = eventType,
Progress = ProgressType.Indeterminate,
Body = null
};
}
public static SignalRMessage CoverUpdateEvent(int id, string entityType)
public static SignalRMessage CoverUpdateEvent(int id, string entityType)
{
return new SignalRMessage()
{
return new SignalRMessage()
Name = CoverUpdate,
Title = "Updating Cover",
Progress = ProgressType.None,
Body = new
{
Name = CoverUpdate,
Title = "Updating Cover",
Progress = ProgressType.None,
Body = new
{
Id = id,
EntityType = entityType,
}
};
}
Id = id,
EntityType = entityType,
}
};
}
public static SignalRMessage UserProgressUpdateEvent(int userId, string username, int seriesId, int volumeId, int chapterId, int pagesRead)
public static SignalRMessage UserProgressUpdateEvent(int userId, string username, int seriesId, int volumeId, int chapterId, int pagesRead)
{
return new SignalRMessage()
{
return new SignalRMessage()
Name = UserProgressUpdate,
Title = "Updating User Progress",
Progress = ProgressType.None,
Body = new
{
Name = UserProgressUpdate,
Title = "Updating User Progress",
Progress = ProgressType.None,
Body = new
{
UserId = userId,
Username = username,
SeriesId = seriesId,
VolumeId = volumeId,
ChapterId = chapterId,
PagesRead = pagesRead,
}
};
}
UserId = userId,
Username = username,
SeriesId = seriesId,
VolumeId = volumeId,
ChapterId = chapterId,
PagesRead = pagesRead,
}
};
}
public static SignalRMessage SiteThemeProgressEvent(string subtitle, string themeName, string eventType)
public static SignalRMessage SiteThemeProgressEvent(string subtitle, string themeName, string eventType)
{
return new SignalRMessage()
{
return new SignalRMessage()
Name = SiteThemeProgress,
Title = "Scanning Site Theme",
SubTitle = subtitle,
EventType = eventType,
Progress = ProgressType.Indeterminate,
Body = new
{
Name = SiteThemeProgress,
Title = "Scanning Site Theme",
SubTitle = subtitle,
EventType = eventType,
Progress = ProgressType.Indeterminate,
Body = new
{
ThemeName = themeName,
}
};
}
ThemeName = themeName,
}
};
}
public static SignalRMessage BookThemeProgressEvent(string subtitle, string themeName, string eventType)
public static SignalRMessage BookThemeProgressEvent(string subtitle, string themeName, string eventType)
{
return new SignalRMessage()
{
return new SignalRMessage()
Name = BookThemeProgress,
Title = "Scanning Book Theme",
SubTitle = subtitle,
EventType = eventType,
Progress = ProgressType.Indeterminate,
Body = new
{
Name = BookThemeProgress,
Title = "Scanning Book Theme",
SubTitle = subtitle,
EventType = eventType,
Progress = ProgressType.Indeterminate,
Body = new
{
ThemeName = themeName,
}
};
}
ThemeName = themeName,
}
};
}
public static SignalRMessage UserUpdateEvent(int userId, string userName)
public static SignalRMessage UserUpdateEvent(int userId, string userName)
{
return new SignalRMessage()
{
return new SignalRMessage()
Name = UserUpdate,
Title = "User Update",
Progress = ProgressType.None,
Body = new
{
Name = UserUpdate,
Title = "User Update",
Progress = ProgressType.None,
Body = new
{
UserId = userId,
UserName = userName
}
};
}
UserId = userId,
UserName = userName
}
};
}
public static SignalRMessage ConvertBookmarksProgressEvent(float progress, string eventType)
public static SignalRMessage ConvertBookmarksProgressEvent(float progress, string eventType)
{
return new SignalRMessage()
{
return new SignalRMessage()
Name = ConvertBookmarksProgress,
Title = "Converting Bookmarks to WebP",
SubTitle = string.Empty,
EventType = eventType,
Progress = ProgressType.Determinate,
Body = new
{
Name = ConvertBookmarksProgress,
Title = "Converting Bookmarks to WebP",
SubTitle = string.Empty,
EventType = eventType,
Progress = ProgressType.Determinate,
Body = new
{
Progress = progress,
EventTime = DateTime.Now
}
};
}
Progress = progress,
EventTime = DateTime.Now
}
};
}
}

View file

@ -1,47 +1,45 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using API.Data;
using API.Extensions;
using API.SignalR.Presence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace API.SignalR
namespace API.SignalR;
/// <summary>
/// Generic hub for sending messages to UI
/// </summary>
[Authorize]
public class MessageHub : Hub
{
/// <summary>
/// Generic hub for sending messages to UI
/// </summary>
[Authorize]
public class MessageHub : Hub
private readonly IPresenceTracker _tracker;
public MessageHub(IPresenceTracker tracker)
{
private readonly IPresenceTracker _tracker;
_tracker = tracker;
}
public MessageHub(IPresenceTracker tracker)
{
_tracker = tracker;
}
public override async Task OnConnectedAsync()
{
await _tracker.UserConnected(Context.User.GetUsername(), Context.ConnectionId);
public override async Task OnConnectedAsync()
{
await _tracker.UserConnected(Context.User.GetUsername(), Context.ConnectionId);
var currentUsers = await PresenceTracker.GetOnlineUsers();
await Clients.All.SendAsync(MessageFactory.OnlineUsers, currentUsers);
var currentUsers = await PresenceTracker.GetOnlineUsers();
await Clients.All.SendAsync(MessageFactory.OnlineUsers, currentUsers);
await base.OnConnectedAsync();
}
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await _tracker.UserDisconnected(Context.User.GetUsername(), Context.ConnectionId);
public override async Task OnDisconnectedAsync(Exception exception)
{
await _tracker.UserDisconnected(Context.User.GetUsername(), Context.ConnectionId);
var currentUsers = await PresenceTracker.GetOnlineUsers();
await Clients.All.SendAsync(MessageFactory.OnlineUsers, currentUsers);
var currentUsers = await PresenceTracker.GetOnlineUsers();
await Clients.All.SendAsync(MessageFactory.OnlineUsers, currentUsers);
await base.OnDisconnectedAsync(exception);
}
await base.OnDisconnectedAsync(exception);
}
}

View file

@ -4,112 +4,111 @@ using System.Linq;
using System.Threading.Tasks;
using API.Data;
namespace API.SignalR.Presence
namespace API.SignalR.Presence;
public interface IPresenceTracker
{
public interface IPresenceTracker
{
Task UserConnected(string username, string connectionId);
Task UserDisconnected(string username, string connectionId);
Task<string[]> GetOnlineAdmins();
Task<List<string>> GetConnectionsForUser(string username);
Task UserConnected(string username, string connectionId);
Task UserDisconnected(string username, string connectionId);
Task<string[]> GetOnlineAdmins();
Task<List<string>> GetConnectionsForUser(string username);
}
internal class ConnectionDetail
{
public List<string> ConnectionIds { get; set; }
public bool IsAdmin { get; set; }
}
// TODO: This can respond to UserRoleUpdate events to handle online users
/// <summary>
/// This is a singleton service for tracking what users have a SignalR connection and their difference connectionIds
/// </summary>
public class PresenceTracker : IPresenceTracker
{
private readonly IUnitOfWork _unitOfWork;
private static readonly Dictionary<string, ConnectionDetail> OnlineUsers = new Dictionary<string, ConnectionDetail>();
public PresenceTracker(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
internal class ConnectionDetail
public async Task UserConnected(string username, string connectionId)
{
public List<string> ConnectionIds { get; set; }
public bool IsAdmin { get; set; }
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username);
if (user == null) return;
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
lock (OnlineUsers)
{
if (OnlineUsers.ContainsKey(username))
{
OnlineUsers[username].ConnectionIds.Add(connectionId);
}
else
{
OnlineUsers.Add(username, new ConnectionDetail()
{
ConnectionIds = new List<string>() {connectionId},
IsAdmin = isAdmin
});
}
}
// Update the last active for the user
user.LastActive = DateTime.Now;
await _unitOfWork.CommitAsync();
}
// TODO: This can respond to UserRoleUpdate events to handle online users
/// <summary>
/// This is a singleton service for tracking what users have a SignalR connection and their difference connectionIds
/// </summary>
public class PresenceTracker : IPresenceTracker
public Task UserDisconnected(string username, string connectionId)
{
private readonly IUnitOfWork _unitOfWork;
private static readonly Dictionary<string, ConnectionDetail> OnlineUsers = new Dictionary<string, ConnectionDetail>();
public PresenceTracker(IUnitOfWork unitOfWork)
lock (OnlineUsers)
{
_unitOfWork = unitOfWork;
}
if (!OnlineUsers.ContainsKey(username)) return Task.CompletedTask;
public async Task UserConnected(string username, string connectionId)
{
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(username);
if (user == null) return;
var isAdmin = await _unitOfWork.UserRepository.IsUserAdminAsync(user);
lock (OnlineUsers)
OnlineUsers[username].ConnectionIds.Remove(connectionId);
if (OnlineUsers[username].ConnectionIds.Count == 0)
{
if (OnlineUsers.ContainsKey(username))
{
OnlineUsers[username].ConnectionIds.Add(connectionId);
}
else
{
OnlineUsers.Add(username, new ConnectionDetail()
{
ConnectionIds = new List<string>() {connectionId},
IsAdmin = isAdmin
});
}
OnlineUsers.Remove(username);
}
// Update the last active for the user
user.LastActive = DateTime.Now;
await _unitOfWork.CommitAsync();
}
return Task.CompletedTask;
}
public Task UserDisconnected(string username, string connectionId)
public static Task<string[]> GetOnlineUsers()
{
string[] onlineUsers;
lock (OnlineUsers)
{
lock (OnlineUsers)
{
if (!OnlineUsers.ContainsKey(username)) return Task.CompletedTask;
OnlineUsers[username].ConnectionIds.Remove(connectionId);
if (OnlineUsers[username].ConnectionIds.Count == 0)
{
OnlineUsers.Remove(username);
}
}
return Task.CompletedTask;
onlineUsers = OnlineUsers.OrderBy(k => k.Key).Select(k => k.Key).ToArray();
}
public static Task<string[]> GetOnlineUsers()
return Task.FromResult(onlineUsers);
}
public Task<string[]> GetOnlineAdmins()
{
// TODO: This might end in stale data, we want to get the online users, query against DB to check if they are admins then return
string[] onlineUsers;
lock (OnlineUsers)
{
string[] onlineUsers;
lock (OnlineUsers)
{
onlineUsers = OnlineUsers.OrderBy(k => k.Key).Select(k => k.Key).ToArray();
}
return Task.FromResult(onlineUsers);
onlineUsers = OnlineUsers.Where(pair => pair.Value.IsAdmin).OrderBy(k => k.Key).Select(k => k.Key).ToArray();
}
public Task<string[]> GetOnlineAdmins()
return Task.FromResult(onlineUsers);
}
public Task<List<string>> GetConnectionsForUser(string username)
{
List<string> connectionIds;
lock (OnlineUsers)
{
// TODO: This might end in stale data, we want to get the online users, query against DB to check if they are admins then return
string[] onlineUsers;
lock (OnlineUsers)
{
onlineUsers = OnlineUsers.Where(pair => pair.Value.IsAdmin).OrderBy(k => k.Key).Select(k => k.Key).ToArray();
}
return Task.FromResult(onlineUsers);
connectionIds = OnlineUsers.GetValueOrDefault(username)?.ConnectionIds;
}
public Task<List<string>> GetConnectionsForUser(string username)
{
List<string> connectionIds;
lock (OnlineUsers)
{
connectionIds = OnlineUsers.GetValueOrDefault(username)?.ConnectionIds;
}
return Task.FromResult(connectionIds ?? new List<string>());
}
return Task.FromResult(connectionIds ?? new List<string>());
}
}

View file

@ -1,39 +1,38 @@
using System;
namespace API.SignalR
namespace API.SignalR;
/// <summary>
/// Payload for SignalR messages to Frontend
/// </summary>
public class SignalRMessage
{
/// <summary>
/// Payload for SignalR messages to Frontend
/// Body of the event type
/// </summary>
public class SignalRMessage
{
/// <summary>
/// Body of the event type
/// </summary>
public object Body { get; set; }
public string Name { get; set; }
/// <summary>
/// User friendly Title of the Event
/// </summary>
/// <example>Scanning Manga</example>
public string Title { get; set; } = string.Empty;
/// <summary>
/// User friendly subtitle. Should have extra info
/// </summary>
/// <example>C:/manga/Accel World V01.cbz</example>
public string SubTitle { get; set; } = string.Empty;
/// <summary>
/// Represents what this represents. started | updated | ended | single
/// <see cref="ProgressEventType"/>
/// </summary>
public string EventType { get; set; } = ProgressEventType.Updated;
/// <summary>
/// How should progress be represented. If Determinate, the Body MUST have a Progress float on it.
/// </summary>
public string Progress { get; set; } = ProgressType.None;
/// <summary>
/// When event took place
/// </summary>
public readonly DateTime EventTime = DateTime.Now;
}
public object Body { get; set; }
public string Name { get; set; }
/// <summary>
/// User friendly Title of the Event
/// </summary>
/// <example>Scanning Manga</example>
public string Title { get; set; } = string.Empty;
/// <summary>
/// User friendly subtitle. Should have extra info
/// </summary>
/// <example>C:/manga/Accel World V01.cbz</example>
public string SubTitle { get; set; } = string.Empty;
/// <summary>
/// Represents what this represents. started | updated | ended | single
/// <see cref="ProgressEventType"/>
/// </summary>
public string EventType { get; set; } = ProgressEventType.Updated;
/// <summary>
/// How should progress be represented. If Determinate, the Body MUST have a Progress float on it.
/// </summary>
public string Progress { get; set; } = ProgressType.None;
/// <summary>
/// When event took place
/// </summary>
public readonly DateTime EventTime = DateTime.Now;
}