Email Version availability (#2345)

This commit is contained in:
Joe Milazzo 2023-10-22 13:19:50 -05:00 committed by GitHub
parent d1157e90c4
commit bcb75ed241
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 137 additions and 39 deletions

View File

@ -42,12 +42,13 @@ public class ServerController : BaseApiController
private readonly IUnitOfWork _unitOfWork; private readonly IUnitOfWork _unitOfWork;
private readonly IEasyCachingProviderFactory _cachingProviderFactory; private readonly IEasyCachingProviderFactory _cachingProviderFactory;
private readonly ILocalizationService _localizationService; private readonly ILocalizationService _localizationService;
private readonly IEmailService _emailService;
public ServerController(ILogger<ServerController> logger, public ServerController(ILogger<ServerController> logger,
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService, IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
ICleanupService cleanupService, IScannerService scannerService, IAccountService accountService, ICleanupService cleanupService, IScannerService scannerService, IAccountService accountService,
ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory,
ILocalizationService localizationService) ILocalizationService localizationService, IEmailService emailService)
{ {
_logger = logger; _logger = logger;
_backupService = backupService; _backupService = backupService;
@ -61,6 +62,7 @@ public class ServerController : BaseApiController
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_cachingProviderFactory = cachingProviderFactory; _cachingProviderFactory = cachingProviderFactory;
_localizationService = localizationService; _localizationService = localizationService;
_emailService = emailService;
} }
/// <summary> /// <summary>
@ -270,5 +272,22 @@ public class ServerController : BaseApiController
return Ok(); return Ok();
} }
/// <summary>
/// Returns the KavitaEmail version for non-default instances
/// </summary>
/// <returns></returns>
[Authorize("RequireAdminRole")]
[HttpGet("email-version")]
public async Task<ActionResult<string?>> GetEmailVersion()
{
var emailServiceUrl = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl))
.Value;
if (emailServiceUrl.Equals(EmailService.DefaultApiUrl)) return Ok(null);
return Ok(await _emailService.GetVersion(emailServiceUrl));
}
} }

View File

@ -26,6 +26,7 @@ public interface IEmailService
Task<EmailTestResultDto> TestConnectivity(string emailUrl, string adminEmail, bool sendEmail); Task<EmailTestResultDto> TestConnectivity(string emailUrl, string adminEmail, bool sendEmail);
Task<bool> IsDefaultEmailService(); Task<bool> IsDefaultEmailService();
Task SendEmailChangeEmail(ConfirmationEmailDto data); Task SendEmailChangeEmail(ConfirmationEmailDto data);
Task<string?> GetVersion(string emailUrl);
} }
public class EmailService : IEmailService public class EmailService : IEmailService
@ -94,6 +95,34 @@ public class EmailService : IEmailService
} }
} }
public async Task<string> GetVersion(string emailUrl)
{
try
{
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var response = await $"{emailUrl}/api/about/version"
.WithHeader("Accept", "application/json")
.WithHeader("User-Agent", "Kavita")
.WithHeader("x-api-key", "MsnvA2DfQqxSK5jh")
.WithHeader("x-kavita-version", BuildInfo.Version)
.WithHeader("x-kavita-installId", settings.InstallId)
.WithHeader("Content-Type", "application/json")
.WithTimeout(TimeSpan.FromSeconds(10))
.GetStringAsync();
if (!string.IsNullOrEmpty(response))
{
return response.Replace("\"", string.Empty);
}
}
catch (Exception)
{
return null;
}
return null;
}
public async Task SendConfirmationEmail(ConfirmationEmailDto data) public async Task SendConfirmationEmail(ConfirmationEmailDto data)
{ {
var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value; var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value;

View File

@ -651,9 +651,10 @@ public class SeriesService : ISeriesService
{ {
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library); var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library);
if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist"));
var libraryIds = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId); if (!(await _unitOfWork.UserRepository.HasAccessToSeries(userId, seriesId)))
if (!libraryIds.Contains(series.LibraryId)) //// TODO: Rewrite this to use a new method which checks permissions all in the DB to be streamlined and less memory {
throw new UnauthorizedAccessException("user-no-access-library-from-series"); throw new UnauthorizedAccessException("user-no-access-library-from-series");
}
if (series?.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) || series.Library.Type == LibraryType.Book) if (series?.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) || series.Library.Type == LibraryType.Book)
{ {
return new NextExpectedChapterDto() return new NextExpectedChapterDto()
@ -670,18 +671,32 @@ public class SeriesService : ISeriesService
.ToList(); .ToList();
// Calculate the time differences between consecutive chapters // Calculate the time differences between consecutive chapters
var timeDifferences = chapters // var timeDifferences = chapters
.Select((chapter, index) => new // .Select((chapter, index) => new
// {
// ChapterNumber = chapter.Number,
// VolumeNumber = chapter.Volume.Number,
// TimeDifference = index == 0 ? TimeSpan.Zero : (chapter.CreatedUtc - chapters.ElementAt(index - 1).CreatedUtc)
// })
// .ToList();
// Quantize time differences: Chapters created within an hour from each other will be treated as one time delta
var timeDifferences = new List<TimeSpan>();
DateTime? previousChapterTime = null;
foreach (var chapter in chapters)
{
if (previousChapterTime.HasValue && (chapter.CreatedUtc - previousChapterTime.Value) <= TimeSpan.FromHours(1))
{ {
ChapterNumber = chapter.Number, continue; // Skip this chapter if it's within an hour of the previous one
VolumeNumber = chapter.Volume.Number, }
TimeDifference = index == 0 ? TimeSpan.Zero : (chapter.CreatedUtc - chapters.ElementAt(index - 1).CreatedUtc) timeDifferences.Add(chapter.CreatedUtc - previousChapterTime ?? TimeSpan.Zero);
}) previousChapterTime = chapter.CreatedUtc;
.ToList(); }
// Calculate the average time difference between chapters // Calculate the average time difference between chapters
// var averageTimeDifference = timeDifferences
// .Average(td => td.TimeDifference.TotalDays);
var averageTimeDifference = timeDifferences var averageTimeDifference = timeDifferences
.Average(td => td.TimeDifference.TotalDays); .Average(td => td.TotalDays);
// Calculate the forecast for when the next chapter is expected // Calculate the forecast for when the next chapter is expected
var nextChapterExpected = chapters.Any() var nextChapterExpected = chapters.Any()
@ -693,8 +708,8 @@ public class SeriesService : ISeriesService
nextChapterExpected = DateTime.UtcNow + TimeSpan.FromDays(averageTimeDifference); nextChapterExpected = DateTime.UtcNow + TimeSpan.FromDays(averageTimeDifference);
} }
var lastChapter = timeDifferences.Last(); var lastChapter = chapters.Last();
float.TryParse(lastChapter.ChapterNumber, NumberStyles.Number, CultureInfo.InvariantCulture, float.TryParse(lastChapter.Number, NumberStyles.Number, CultureInfo.InvariantCulture,
out var lastChapterNumber); out var lastChapterNumber);
var result = new NextExpectedChapterDto() var result = new NextExpectedChapterDto()
@ -708,7 +723,7 @@ public class SeriesService : ISeriesService
if (lastChapterNumber > 0) if (lastChapterNumber > 0)
{ {
result.ChapterNumber = lastChapterNumber + 1; result.ChapterNumber = lastChapterNumber + 1;
result.VolumeNumber = lastChapter.VolumeNumber; result.VolumeNumber = lastChapter.Volume.Number;
result.Title = series.Library.Type switch result.Title = series.Library.Type switch
{ {
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num",
@ -722,8 +737,8 @@ public class SeriesService : ISeriesService
} }
else else
{ {
result.VolumeNumber = lastChapter.VolumeNumber + 1; result.VolumeNumber = lastChapter.Volume.Number + 1;
result.Title = await _localizationService.Translate(userId, "vol-num", result.Title = await _localizationService.Translate(userId, "volume-num",
new object[] {result.VolumeNumber}); new object[] {result.VolumeNumber});
} }

View File

@ -238,21 +238,8 @@ public class Startup
logger.LogInformation("Running Migrations"); logger.LogInformation("Running Migrations");
// v0.7.2
await MigrateLoginRoles.Migrate(unitOfWork, userManager, logger);
// v0.7.3
await MigrateRemoveWebPSettingRows.Migrate(unitOfWork, logger);
// v0.7.4
await MigrateDisableScrobblingOnComicLibraries.Migrate(unitOfWork, dataContext, logger);
// v0.7.6
await MigrateExistingRatings.Migrate(dataContext, logger);
// v0.7.9 // v0.7.9
await MigrateUserLibrarySideNavStream.Migrate(unitOfWork, dataContext, logger); await MigrateUserLibrarySideNavStream.Migrate(unitOfWork, dataContext, logger);
await MigrateDashboardStreamNamesToLocaleKeys.Migrate(unitOfWork, dataContext, logger);
// Update the version in the DB after all migrations are run // Update the version in the DB after all migrations are run
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);

View File

@ -5,6 +5,7 @@ import {ServerInfoSlim} from '../admin/_models/server-info';
import { UpdateVersionEvent } from '../_models/events/update-version-event'; import { UpdateVersionEvent } from '../_models/events/update-version-event';
import { Job } from '../_models/job/job'; import { Job } from '../_models/job/job';
import { KavitaMediaError } from '../admin/_models/media-error'; import { KavitaMediaError } from '../admin/_models/media-error';
import {TextResonse} from "../_types/text-response";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -67,4 +68,8 @@ export class ServerService {
clearMediaAlerts() { clearMediaAlerts() {
return this.httpClient.post(this.baseUrl + 'server/clear-media-alerts', {}); return this.httpClient.post(this.baseUrl + 'server/clear-media-alerts', {});
} }
getEmailVersion() {
return this.httpClient.get<string>(this.baseUrl + 'server/email-version', TextResonse);
}
} }

View File

@ -1,13 +1,14 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {ToastrService} from 'ngx-toastr'; import {ToastrService} from 'ngx-toastr';
import {take} from 'rxjs'; import {forkJoin, take} from 'rxjs';
import {EmailTestResult, SettingsService} from '../settings.service'; import {EmailTestResult, SettingsService} from '../settings.service';
import {ServerSettings} from '../_models/server-settings'; import {ServerSettings} from '../_models/server-settings';
import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap'; import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap';
import {NgIf, NgTemplateOutlet} from '@angular/common'; import {NgIf, NgTemplateOutlet} from '@angular/common';
import {TranslocoModule, TranslocoService} from "@ngneat/transloco"; import {translate, TranslocoModule, TranslocoService} from "@ngneat/transloco";
import {SafeHtmlPipe} from "../../pipe/safe-html.pipe"; import {SafeHtmlPipe} from "../../pipe/safe-html.pipe";
import {ServerService} from "../../_services/server.service";
@Component({ @Component({
selector: 'app-manage-email-settings', selector: 'app-manage-email-settings',
@ -22,9 +23,13 @@ export class ManageEmailSettingsComponent implements OnInit {
serverSettings!: ServerSettings; serverSettings!: ServerSettings;
settingsForm: FormGroup = new FormGroup({}); settingsForm: FormGroup = new FormGroup({});
link = '<a href="https://github.com/Kareadita/KavitaEmail" target="_blank" rel="noopener noreferrer">Kavita Email</a>'; link = '<a href="https://github.com/Kareadita/KavitaEmail" target="_blank" rel="noopener noreferrer">Kavita Email</a>';
emailVersion: string | null = null;
private readonly cdRef = inject(ChangeDetectorRef); private readonly cdRef = inject(ChangeDetectorRef);
private readonly serverService = inject(ServerService);
private readonly settingsService = inject(SettingsService);
private readonly toastr = inject(ToastrService);
constructor(private settingsService: SettingsService, private toastr: ToastrService, private translocoService: TranslocoService) { } constructor() { }
ngOnInit(): void { ngOnInit(): void {
this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
@ -33,6 +38,11 @@ export class ManageEmailSettingsComponent implements OnInit {
this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [])); this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, []));
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
this.serverService.getEmailVersion().subscribe(version => {
this.emailVersion = version;
this.cdRef.markForCheck();
});
} }
resetForm() { resetForm() {
@ -51,7 +61,7 @@ export class ManageEmailSettingsComponent implements OnInit {
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => { this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings; this.serverSettings = settings;
this.resetForm(); this.resetForm();
this.toastr.success(this.translocoService.translate('toasts.server-settings-updated')); this.toastr.success(translate('toasts.server-settings-updated'));
}, (err: any) => { }, (err: any) => {
console.error('error: ', err); console.error('error: ', err);
}); });
@ -61,7 +71,7 @@ export class ManageEmailSettingsComponent implements OnInit {
this.settingsService.resetServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { this.settingsService.resetServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings = settings; this.serverSettings = settings;
this.resetForm(); this.resetForm();
this.toastr.success(this.translocoService.translate('toasts.server-settings-updated')); this.toastr.success(translate('toasts.server-settings-updated'));
}, (err: any) => { }, (err: any) => {
console.error('error: ', err); console.error('error: ', err);
}); });
@ -71,7 +81,7 @@ export class ManageEmailSettingsComponent implements OnInit {
this.settingsService.resetEmailServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { this.settingsService.resetEmailServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => {
this.serverSettings.emailServiceUrl = settings.emailServiceUrl; this.serverSettings.emailServiceUrl = settings.emailServiceUrl;
this.resetForm(); this.resetForm();
this.toastr.success(this.translocoService.translate('toasts.email-service-reset')); this.toastr.success(translate('toasts.email-service-reset'));
}, (err: any) => { }, (err: any) => {
console.error('error: ', err); console.error('error: ', err);
}); });
@ -79,11 +89,14 @@ export class ManageEmailSettingsComponent implements OnInit {
testEmailServiceUrl() { testEmailServiceUrl() {
if (this.settingsForm.get('emailServiceUrl')?.value === '') return; if (this.settingsForm.get('emailServiceUrl')?.value === '') return;
this.settingsService.testEmailServerSettings(this.settingsForm.get('emailServiceUrl')?.value).pipe(take(1)).subscribe(async (result: EmailTestResult) => { forkJoin([this.settingsService.testEmailServerSettings(this.settingsForm.get('emailServiceUrl')?.value), this.serverService.getEmailVersion()])
.pipe(take(1)).subscribe(async (results) => {
const result = results[0] as EmailTestResult;
if (result.successful) { if (result.successful) {
this.toastr.success(this.translocoService.translate('toasts.email-service-reachable')); const version = ('. Kavita Email: ' + results[1] ? 'v' + results[1] : '');
this.toastr.success(translate('toasts.email-service-reachable') + version);
} else { } else {
this.toastr.error(this.translocoService.translate('toasts.email-service-unresponsive') + result.errorMessage); this.toastr.error(translate('toasts.email-service-unresponsive') + result.errorMessage.split('(')[0]);
} }
}, (err: any) => { }, (err: any) => {

View File

@ -1907,7 +1907,7 @@
"reading-list-imported": "Reading List imported", "reading-list-imported": "Reading List imported",
"incognito-off": "Incognito mode is off. Progress will now start being tracked.", "incognito-off": "Incognito mode is off. Progress will now start being tracked.",
"email-service-reset": "Email Service Reset", "email-service-reset": "Email Service Reset",
"email-service-reachable": "Email Service was reachable", "email-service-reachable": "Kavita Email Connection Successful",
"email-service-unresponsive": "Email Service Url did not respond.", "email-service-unresponsive": "Email Service Url did not respond.",
"refresh-covers-queued": "Refresh covers queued for {{name}}", "refresh-covers-queued": "Refresh covers queued for {{name}}",
"library-file-analysis-queued": "Library file analysis queued for {{name}}", "library-file-analysis-queued": "Library file analysis queued for {{name}}",

View File

@ -9455,6 +9455,36 @@
} }
} }
}, },
"/api/Server/email-version": {
"get": {
"tags": [
"Server"
],
"summary": "Returns the KavitaEmail version for non-default instances",
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
},
"application/json": {
"schema": {
"type": "string"
}
},
"text/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/api/Settings/base-url": { "/api/Settings/base-url": {
"get": { "get": {
"tags": [ "tags": [