diff --git a/API/CoverFormatRequestRecord.cs b/API/APIEndpointRecords/CoverFormatRequestRecord.cs similarity index 84% rename from API/CoverFormatRequestRecord.cs rename to API/APIEndpointRecords/CoverFormatRequestRecord.cs index 429761a..36eaa0f 100644 --- a/API/CoverFormatRequestRecord.cs +++ b/API/APIEndpointRecords/CoverFormatRequestRecord.cs @@ -1,7 +1,6 @@ using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats; -namespace API; +namespace API.APIEndpointRecords; public record CoverFormatRequestRecord(Size size) { diff --git a/API/APIEndpointRecords/GotifyRecord.cs b/API/APIEndpointRecords/GotifyRecord.cs new file mode 100644 index 0000000..59c5287 --- /dev/null +++ b/API/APIEndpointRecords/GotifyRecord.cs @@ -0,0 +1,16 @@ +namespace API.APIEndpointRecords; + +public record GotifyRecord(string endpoint, string appToken, int priority) +{ + public bool Validate() + { + if (endpoint == string.Empty) + return false; + if (appToken == string.Empty) + return false; + if (priority < 0 || priority > 10) + return false; + + return true; + } +} \ No newline at end of file diff --git a/API/APIEndpointRecords/LunaseaRecord.cs b/API/APIEndpointRecords/LunaseaRecord.cs new file mode 100644 index 0000000..9bd1a53 --- /dev/null +++ b/API/APIEndpointRecords/LunaseaRecord.cs @@ -0,0 +1,16 @@ +using System.Text.RegularExpressions; + +namespace API.APIEndpointRecords; + +public record LunaseaRecord(string id) +{ + private static Regex validateRex = new(@"(?:device|user)\/[0-9a-zA-Z\-]+"); + public bool Validate() + { + if (id == string.Empty) + return false; + if (!validateRex.IsMatch(id)) + return false; + return true; + } +} \ No newline at end of file diff --git a/API/ModifyJobRecord.cs b/API/APIEndpointRecords/ModifyJobRecord.cs similarity index 66% rename from API/ModifyJobRecord.cs rename to API/APIEndpointRecords/ModifyJobRecord.cs index 28b0260..4e110fa 100644 --- a/API/ModifyJobRecord.cs +++ b/API/APIEndpointRecords/ModifyJobRecord.cs @@ -1,3 +1,3 @@ -namespace API; +namespace API.APIEndpointRecords; public record ModifyJobRecord(ulong? RecurrenceMs, bool? Enabled); \ No newline at end of file diff --git a/API/APIEndpointRecords/NtfyRecord.cs b/API/APIEndpointRecords/NtfyRecord.cs new file mode 100644 index 0000000..a8c2c22 --- /dev/null +++ b/API/APIEndpointRecords/NtfyRecord.cs @@ -0,0 +1,17 @@ +namespace API.APIEndpointRecords; + +public record NtfyRecord(string endpoint, string username, string password, string topic, int priority) +{ + public bool Validate() + { + if (endpoint == string.Empty) + return false; + if (username == string.Empty) + return false; + if (password == string.Empty) + return false; + if (priority < 1 || priority > 5) + return false; + return true; + } +} \ No newline at end of file diff --git a/API/Controllers/JobController.cs b/API/Controllers/JobController.cs index 85d994d..d321c6c 100644 --- a/API/Controllers/JobController.cs +++ b/API/Controllers/JobController.cs @@ -1,4 +1,5 @@ -using API.Schema; +using API.APIEndpointRecords; +using API.Schema; using API.Schema.Jobs; using Asp.Versioning; using Microsoft.AspNetCore.Mvc; diff --git a/API/Controllers/MangaController.cs b/API/Controllers/MangaController.cs index d1cf9ba..2b205cd 100644 --- a/API/Controllers/MangaController.cs +++ b/API/Controllers/MangaController.cs @@ -1,4 +1,5 @@ -using API.Schema; +using API.APIEndpointRecords; +using API.Schema; using Asp.Versioning; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; diff --git a/API/Controllers/NotificationConnectorController.cs b/API/Controllers/NotificationConnectorController.cs index f6c7ca7..e9adddb 100644 --- a/API/Controllers/NotificationConnectorController.cs +++ b/API/Controllers/NotificationConnectorController.cs @@ -1,4 +1,6 @@ -using API.Schema; +using System.Text; +using API.APIEndpointRecords; +using API.Schema; using API.Schema.NotificationConnectors; using Asp.Versioning; using Microsoft.AspNetCore.Mvc; @@ -44,16 +46,21 @@ public class NotificationConnectorController(PgsqlContext context) : Controller } /// - /// Creates a new Notification-Connector + /// Creates a new REST-Notification-Connector /// + /// Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent /// Notification-Connector /// + /// A NotificationConnector with name already exists /// Error during Database Operation [HttpPut] - [ProducesResponseType(Status200OK, "application/json")] + [ProducesResponseType(Status201Created)] + [ProducesResponseType(Status409Conflict)] [ProducesResponseType(Status500InternalServerError, "text/plain")] public IActionResult CreateConnector([FromBody]NotificationConnector notificationConnector) { + if (context.NotificationConnectors.Find(notificationConnector.Name) is not null) + return Conflict(); try { context.NotificationConnectors.Add(notificationConnector); @@ -66,6 +73,91 @@ public class NotificationConnectorController(PgsqlContext context) : Controller } } + /// + /// Creates a new Gotify-Notification-Connector + /// + /// Priority needs to be between 0 and 10 + /// + /// + /// A NotificationConnector with name already exists + /// Error during Database Operation + [HttpPut("Gotify")] + [ProducesResponseType(Status201Created)] + [ProducesResponseType(Status400BadRequest)] + [ProducesResponseType(Status409Conflict)] + [ProducesResponseType(Status500InternalServerError, "text/plain")] + public IActionResult CreateGotifyConnector([FromBody]GotifyRecord gotifyData) + { + if(!gotifyData.Validate()) + return BadRequest(); + + NotificationConnector gotifyConnector = new NotificationConnector(TokenGen.CreateToken("Gotify"), + gotifyData.endpoint, + new Dictionary() { { "X-Gotify-Key", gotifyData.appToken } }, + "POST", + $"{{\"message\": \"%text\", \"title\": \"%title\", \"priority\": {gotifyData.priority}}}"); + return CreateConnector(gotifyConnector); + } + + /// + /// Creates a new Ntfy-Notification-Connector + /// + /// Priority needs to be between 1 and 5 + /// + /// + /// A NotificationConnector with name already exists + /// Error during Database Operation + [HttpPut("Ntfy")] + [ProducesResponseType(Status201Created)] + [ProducesResponseType(Status400BadRequest)] + [ProducesResponseType(Status409Conflict)] + [ProducesResponseType(Status500InternalServerError, "text/plain")] + public IActionResult CreateNtfyConnector([FromBody]NtfyRecord ntfyRecord) + { + if(!ntfyRecord.Validate()) + return BadRequest(); + + string authHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{ntfyRecord.username}:{ntfyRecord.password}")); + string auth = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=",""); + + NotificationConnector ntfyConnector = new NotificationConnector(TokenGen.CreateToken("Ntfy"), + $"{ntfyRecord.endpoint}?auth={auth}", + new Dictionary() + { + {"Title", "%title"}, + {"Priority", ntfyRecord.priority.ToString()}, + }, + "POST", + "%text"); + return CreateConnector(ntfyConnector); + } + + /// + /// Creates a new Lunasea-Notification-Connector + /// + /// https://docs.lunasea.app/lunasea/notifications/custom-notifications for id. Either device/:device_id or user/:user_id + /// + /// + /// A NotificationConnector with name already exists + /// Error during Database Operation + [HttpPut("Lunasea")] + [ProducesResponseType(Status201Created)] + [ProducesResponseType(Status400BadRequest)] + [ProducesResponseType(Status409Conflict)] + [ProducesResponseType(Status500InternalServerError, "text/plain")] + public IActionResult CreateLunaseaConnector([FromBody]LunaseaRecord lunaseaRecord) + { + if(!lunaseaRecord.Validate()) + return BadRequest(); + + NotificationConnector lunaseaConnector = new NotificationConnector(TokenGen.CreateToken("Lunasea"), + $"https://notify.lunasea.app/v1/custom/{lunaseaRecord.id}", + new Dictionary(), + "POST", + "{\"title\": \"%title\", \"body\": \"%text\"}"); + return CreateConnector(lunaseaConnector); + } + /// /// Deletes the Notification-Connector with the requested ID /// diff --git a/API/Migrations/20250307143019_dev-070325-3.Designer.cs b/API/Migrations/20250307143019_dev-070325-3.Designer.cs new file mode 100644 index 0000000..3098d08 --- /dev/null +++ b/API/Migrations/20250307143019_dev-070325-3.Designer.cs @@ -0,0 +1,669 @@ +// +using System; +using System.Collections.Generic; +using API.Schema; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace API.Migrations +{ + [DbContext(typeof(PgsqlContext))] + [Migration("20250307143019_dev-070325-3")] + partial class dev0703253 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("API.Schema.Author", b => + { + b.Property("AuthorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("AuthorId"); + + b.ToTable("Authors"); + }); + + modelBuilder.Entity("API.Schema.Chapter", b => + { + b.Property("ChapterId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ArchiveFileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChapterNumber") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Downloaded") + .HasColumnType("boolean"); + + b.Property("ParentMangaId") + .IsRequired() + .HasColumnType("character varying(64)"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("VolumeNumber") + .HasColumnType("integer"); + + b.HasKey("ChapterId"); + + b.HasIndex("ParentMangaId"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("API.Schema.Jobs.Job", b => + { + b.Property("JobId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.PrimitiveCollection("DependsOnJobsIds") + .HasMaxLength(64) + .HasColumnType("text[]"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("JobId1") + .HasColumnType("character varying(64)"); + + b.Property("JobType") + .HasColumnType("smallint"); + + b.Property("LastExecution") + .HasColumnType("timestamp with time zone"); + + b.Property("ParentJobId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RecurrenceMs") + .HasColumnType("numeric(20,0)"); + + b.Property("state") + .HasColumnType("smallint"); + + b.HasKey("JobId"); + + b.HasIndex("JobId1"); + + b.HasIndex("ParentJobId"); + + b.ToTable("Jobs"); + + b.HasDiscriminator("JobType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b => + { + b.Property("LibraryConnectorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("LibraryType") + .HasColumnType("smallint"); + + b.HasKey("LibraryConnectorId"); + + b.ToTable("LibraryConnectors"); + + b.HasDiscriminator("LibraryType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.Link", b => + { + b.Property("LinkId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LinkProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("LinkUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.HasKey("LinkId"); + + b.HasIndex("MangaId"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.Property("MangaId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConnectorId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CoverFileNameInCache") + .HasColumnType("text"); + + b.Property("CoverUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("FolderName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IgnoreChapterBefore") + .HasColumnType("real"); + + b.Property("MangaConnectorId") + .IsRequired() + .HasColumnType("character varying(32)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OriginalLanguage") + .HasColumnType("text"); + + b.Property("ReleaseStatus") + .HasColumnType("smallint"); + + b.Property("WebsiteUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("bigint"); + + b.HasKey("MangaId"); + + b.HasIndex("MangaConnectorId"); + + b.ToTable("Manga"); + }); + + modelBuilder.Entity("API.Schema.MangaAltTitle", b => + { + b.Property("AltTitleId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("AltTitleId"); + + b.HasIndex("MangaId"); + + b.ToTable("AltTitles"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => + { + b.Property("Name") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.PrimitiveCollection("BaseUris") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.PrimitiveCollection("SupportedLanguages") + .IsRequired() + .HasColumnType("text[]"); + + b.HasKey("Name"); + + b.ToTable("MangaConnectors"); + + b.HasDiscriminator("Name").HasValue("MangaConnector"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.MangaTag", b => + { + b.Property("Tag") + .HasColumnType("text"); + + b.HasKey("Tag"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("API.Schema.Notification", b => + { + b.Property("NotificationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Urgency") + .HasColumnType("smallint"); + + b.HasKey("NotificationId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => + { + b.Property("NotificationConnectorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property>("Headers") + .IsRequired() + .HasColumnType("hstore"); + + b.Property("HttpMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("NotificationConnectorId"); + + b.ToTable("NotificationConnectors"); + }); + + modelBuilder.Entity("AuthorManga", b => + { + b.Property("AuthorsAuthorId") + .HasColumnType("character varying(64)"); + + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.HasKey("AuthorsAuthorId", "MangaId"); + + b.HasIndex("MangaId"); + + b.ToTable("AuthorManga"); + }); + + modelBuilder.Entity("MangaMangaTag", b => + { + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.Property("TagsTag") + .HasColumnType("text"); + + b.HasKey("MangaId", "TagsTag"); + + b.HasIndex("TagsTag"); + + b.ToTable("MangaMangaTag"); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("MangaId"); + + b.ToTable("Jobs", t => + { + t.Property("MangaId") + .HasColumnName("DownloadMangaCoverJob_MangaId"); + }); + + b.HasDiscriminator().HasValue((byte)4); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("MangaId"); + + b.HasDiscriminator().HasValue((byte)1); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("ChapterId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("ChapterId"); + + b.HasDiscriminator().HasValue((byte)0); + }); + + modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("FromLocation") + .IsRequired() + .HasColumnType("text"); + + b.Property("ToLocation") + .IsRequired() + .HasColumnType("text"); + + b.HasDiscriminator().HasValue((byte)3); + }); + + modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("MangaId"); + + b.ToTable("Jobs", t => + { + t.Property("MangaId") + .HasColumnName("UpdateMetadataJob_MangaId"); + }); + + b.HasDiscriminator().HasValue((byte)2); + }); + + modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b => + { + b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); + + b.HasDiscriminator().HasValue((byte)1); + }); + + modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b => + { + b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); + + b.HasDiscriminator().HasValue((byte)0); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("AsuraToon"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Bato"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaDex"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaHere"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaKatana"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Mangaworld"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("ManhuaPlus"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Weebcentral"); + }); + + modelBuilder.Entity("API.Schema.Chapter", b => + { + b.HasOne("API.Schema.Manga", "ParentManga") + .WithMany() + .HasForeignKey("ParentMangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ParentManga"); + }); + + modelBuilder.Entity("API.Schema.Jobs.Job", b => + { + b.HasOne("API.Schema.Jobs.Job", null) + .WithMany("DependsOnJobs") + .HasForeignKey("JobId1"); + + b.HasOne("API.Schema.Jobs.Job", "ParentJob") + .WithMany() + .HasForeignKey("ParentJobId"); + + b.Navigation("ParentJob"); + }); + + modelBuilder.Entity("API.Schema.Link", b => + { + b.HasOne("API.Schema.Manga", null) + .WithMany("Links") + .HasForeignKey("MangaId"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") + .WithMany() + .HasForeignKey("MangaConnectorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MangaConnector"); + }); + + modelBuilder.Entity("API.Schema.MangaAltTitle", b => + { + b.HasOne("API.Schema.Manga", null) + .WithMany("AltTitles") + .HasForeignKey("MangaId"); + }); + + modelBuilder.Entity("AuthorManga", b => + { + b.HasOne("API.Schema.Author", null) + .WithMany() + .HasForeignKey("AuthorsAuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.Manga", null) + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MangaMangaTag", b => + { + b.HasOne("API.Schema.Manga", null) + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.MangaTag", null) + .WithMany() + .HasForeignKey("TagsTag") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b => + { + b.HasOne("API.Schema.Manga", "Manga") + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Manga"); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b => + { + b.HasOne("API.Schema.Manga", "Manga") + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Manga"); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b => + { + b.HasOne("API.Schema.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b => + { + b.HasOne("API.Schema.Manga", "Manga") + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Manga"); + }); + + modelBuilder.Entity("API.Schema.Jobs.Job", b => + { + b.Navigation("DependsOnJobs"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.Navigation("AltTitles"); + + b.Navigation("Links"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Migrations/20250307143019_dev-070325-3.cs b/API/Migrations/20250307143019_dev-070325-3.cs new file mode 100644 index 0000000..0093ddd --- /dev/null +++ b/API/Migrations/20250307143019_dev-070325-3.cs @@ -0,0 +1,139 @@ +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Migrations +{ + /// + public partial class dev0703253 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AppToken", + table: "NotificationConnectors"); + + migrationBuilder.DropColumn( + name: "Auth", + table: "NotificationConnectors"); + + migrationBuilder.DropColumn( + name: "Endpoint", + table: "NotificationConnectors"); + + migrationBuilder.DropColumn( + name: "Id", + table: "NotificationConnectors"); + + migrationBuilder.DropColumn( + name: "NotificationConnectorType", + table: "NotificationConnectors"); + + migrationBuilder.DropColumn( + name: "Ntfy_Endpoint", + table: "NotificationConnectors"); + + migrationBuilder.DropColumn( + name: "Topic", + table: "NotificationConnectors"); + + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:PostgresExtension:hstore", ",,"); + + migrationBuilder.AddColumn( + name: "Body", + table: "NotificationConnectors", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn>( + name: "Headers", + table: "NotificationConnectors", + type: "hstore", + nullable: false); + + migrationBuilder.AddColumn( + name: "HttpMethod", + table: "NotificationConnectors", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Url", + table: "NotificationConnectors", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Body", + table: "NotificationConnectors"); + + migrationBuilder.DropColumn( + name: "Headers", + table: "NotificationConnectors"); + + migrationBuilder.DropColumn( + name: "HttpMethod", + table: "NotificationConnectors"); + + migrationBuilder.DropColumn( + name: "Url", + table: "NotificationConnectors"); + + migrationBuilder.AlterDatabase() + .OldAnnotation("Npgsql:PostgresExtension:hstore", ",,"); + + migrationBuilder.AddColumn( + name: "AppToken", + table: "NotificationConnectors", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Auth", + table: "NotificationConnectors", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Endpoint", + table: "NotificationConnectors", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Id", + table: "NotificationConnectors", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "NotificationConnectorType", + table: "NotificationConnectors", + type: "smallint", + nullable: false, + defaultValue: (byte)0); + + migrationBuilder.AddColumn( + name: "Ntfy_Endpoint", + table: "NotificationConnectors", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Topic", + table: "NotificationConnectors", + type: "text", + nullable: true); + } + } +} diff --git a/API/Migrations/20250307143254_dev-070325-4.Designer.cs b/API/Migrations/20250307143254_dev-070325-4.Designer.cs new file mode 100644 index 0000000..db107e3 --- /dev/null +++ b/API/Migrations/20250307143254_dev-070325-4.Designer.cs @@ -0,0 +1,673 @@ +// +using System; +using System.Collections.Generic; +using API.Schema; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace API.Migrations +{ + [DbContext(typeof(PgsqlContext))] + [Migration("20250307143254_dev-070325-4")] + partial class dev0703254 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("API.Schema.Author", b => + { + b.Property("AuthorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("AuthorId"); + + b.ToTable("Authors"); + }); + + modelBuilder.Entity("API.Schema.Chapter", b => + { + b.Property("ChapterId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ArchiveFileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChapterNumber") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Downloaded") + .HasColumnType("boolean"); + + b.Property("ParentMangaId") + .IsRequired() + .HasColumnType("character varying(64)"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("VolumeNumber") + .HasColumnType("integer"); + + b.HasKey("ChapterId"); + + b.HasIndex("ParentMangaId"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("API.Schema.Jobs.Job", b => + { + b.Property("JobId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.PrimitiveCollection("DependsOnJobsIds") + .HasMaxLength(64) + .HasColumnType("text[]"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("JobId1") + .HasColumnType("character varying(64)"); + + b.Property("JobType") + .HasColumnType("smallint"); + + b.Property("LastExecution") + .HasColumnType("timestamp with time zone"); + + b.Property("ParentJobId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RecurrenceMs") + .HasColumnType("numeric(20,0)"); + + b.Property("state") + .HasColumnType("smallint"); + + b.HasKey("JobId"); + + b.HasIndex("JobId1"); + + b.HasIndex("ParentJobId"); + + b.ToTable("Jobs"); + + b.HasDiscriminator("JobType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b => + { + b.Property("LibraryConnectorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("LibraryType") + .HasColumnType("smallint"); + + b.HasKey("LibraryConnectorId"); + + b.ToTable("LibraryConnectors"); + + b.HasDiscriminator("LibraryType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.Link", b => + { + b.Property("LinkId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LinkProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("LinkUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.HasKey("LinkId"); + + b.HasIndex("MangaId"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.Property("MangaId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConnectorId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CoverFileNameInCache") + .HasColumnType("text"); + + b.Property("CoverUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("FolderName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IgnoreChapterBefore") + .HasColumnType("real"); + + b.Property("MangaConnectorId") + .IsRequired() + .HasColumnType("character varying(32)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OriginalLanguage") + .HasColumnType("text"); + + b.Property("ReleaseStatus") + .HasColumnType("smallint"); + + b.Property("WebsiteUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("bigint"); + + b.HasKey("MangaId"); + + b.HasIndex("MangaConnectorId"); + + b.ToTable("Manga"); + }); + + modelBuilder.Entity("API.Schema.MangaAltTitle", b => + { + b.Property("AltTitleId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("AltTitleId"); + + b.HasIndex("MangaId"); + + b.ToTable("AltTitles"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => + { + b.Property("Name") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.PrimitiveCollection("BaseUris") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.PrimitiveCollection("SupportedLanguages") + .IsRequired() + .HasColumnType("text[]"); + + b.HasKey("Name"); + + b.ToTable("MangaConnectors"); + + b.HasDiscriminator("Name").HasValue("MangaConnector"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.MangaTag", b => + { + b.Property("Tag") + .HasColumnType("text"); + + b.HasKey("Tag"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("API.Schema.Notification", b => + { + b.Property("NotificationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Urgency") + .HasColumnType("smallint"); + + b.HasKey("NotificationId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => + { + b.Property("NotificationConnectorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property>("Headers") + .IsRequired() + .HasColumnType("hstore"); + + b.Property("HttpMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("NotificationConnectorId"); + + b.ToTable("NotificationConnectors"); + }); + + modelBuilder.Entity("AuthorManga", b => + { + b.Property("AuthorsAuthorId") + .HasColumnType("character varying(64)"); + + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.HasKey("AuthorsAuthorId", "MangaId"); + + b.HasIndex("MangaId"); + + b.ToTable("AuthorManga"); + }); + + modelBuilder.Entity("MangaMangaTag", b => + { + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.Property("TagsTag") + .HasColumnType("text"); + + b.HasKey("MangaId", "TagsTag"); + + b.HasIndex("TagsTag"); + + b.ToTable("MangaMangaTag"); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("MangaId"); + + b.ToTable("Jobs", t => + { + t.Property("MangaId") + .HasColumnName("DownloadMangaCoverJob_MangaId"); + }); + + b.HasDiscriminator().HasValue((byte)4); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("MangaId"); + + b.HasDiscriminator().HasValue((byte)1); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("ChapterId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("ChapterId"); + + b.HasDiscriminator().HasValue((byte)0); + }); + + modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("FromLocation") + .IsRequired() + .HasColumnType("text"); + + b.Property("ToLocation") + .IsRequired() + .HasColumnType("text"); + + b.HasDiscriminator().HasValue((byte)3); + }); + + modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("MangaId"); + + b.ToTable("Jobs", t => + { + t.Property("MangaId") + .HasColumnName("UpdateMetadataJob_MangaId"); + }); + + b.HasDiscriminator().HasValue((byte)2); + }); + + modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b => + { + b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); + + b.HasDiscriminator().HasValue((byte)1); + }); + + modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b => + { + b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); + + b.HasDiscriminator().HasValue((byte)0); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("AsuraToon"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Bato"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaDex"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaHere"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaKatana"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Mangaworld"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("ManhuaPlus"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Weebcentral"); + }); + + modelBuilder.Entity("API.Schema.Chapter", b => + { + b.HasOne("API.Schema.Manga", "ParentManga") + .WithMany() + .HasForeignKey("ParentMangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ParentManga"); + }); + + modelBuilder.Entity("API.Schema.Jobs.Job", b => + { + b.HasOne("API.Schema.Jobs.Job", null) + .WithMany("DependsOnJobs") + .HasForeignKey("JobId1"); + + b.HasOne("API.Schema.Jobs.Job", "ParentJob") + .WithMany() + .HasForeignKey("ParentJobId"); + + b.Navigation("ParentJob"); + }); + + modelBuilder.Entity("API.Schema.Link", b => + { + b.HasOne("API.Schema.Manga", null) + .WithMany("Links") + .HasForeignKey("MangaId"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") + .WithMany() + .HasForeignKey("MangaConnectorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MangaConnector"); + }); + + modelBuilder.Entity("API.Schema.MangaAltTitle", b => + { + b.HasOne("API.Schema.Manga", null) + .WithMany("AltTitles") + .HasForeignKey("MangaId"); + }); + + modelBuilder.Entity("AuthorManga", b => + { + b.HasOne("API.Schema.Author", null) + .WithMany() + .HasForeignKey("AuthorsAuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.Manga", null) + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MangaMangaTag", b => + { + b.HasOne("API.Schema.Manga", null) + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.MangaTag", null) + .WithMany() + .HasForeignKey("TagsTag") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b => + { + b.HasOne("API.Schema.Manga", "Manga") + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Manga"); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b => + { + b.HasOne("API.Schema.Manga", "Manga") + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Manga"); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b => + { + b.HasOne("API.Schema.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b => + { + b.HasOne("API.Schema.Manga", "Manga") + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Manga"); + }); + + modelBuilder.Entity("API.Schema.Jobs.Job", b => + { + b.Navigation("DependsOnJobs"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.Navigation("AltTitles"); + + b.Navigation("Links"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Migrations/20250307143254_dev-070325-4.cs b/API/Migrations/20250307143254_dev-070325-4.cs new file mode 100644 index 0000000..259e602 --- /dev/null +++ b/API/Migrations/20250307143254_dev-070325-4.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Migrations +{ + /// + public partial class dev0703254 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Name", + table: "NotificationConnectors", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Name", + table: "NotificationConnectors"); + } + } +} diff --git a/API/Migrations/20250307143442_dev-070325-5.Designer.cs b/API/Migrations/20250307143442_dev-070325-5.Designer.cs new file mode 100644 index 0000000..87408bc --- /dev/null +++ b/API/Migrations/20250307143442_dev-070325-5.Designer.cs @@ -0,0 +1,669 @@ +// +using System; +using System.Collections.Generic; +using API.Schema; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace API.Migrations +{ + [DbContext(typeof(PgsqlContext))] + [Migration("20250307143442_dev-070325-5")] + partial class dev0703255 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("API.Schema.Author", b => + { + b.Property("AuthorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("AuthorId"); + + b.ToTable("Authors"); + }); + + modelBuilder.Entity("API.Schema.Chapter", b => + { + b.Property("ChapterId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ArchiveFileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("ChapterNumber") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Downloaded") + .HasColumnType("boolean"); + + b.Property("ParentMangaId") + .IsRequired() + .HasColumnType("character varying(64)"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("VolumeNumber") + .HasColumnType("integer"); + + b.HasKey("ChapterId"); + + b.HasIndex("ParentMangaId"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("API.Schema.Jobs.Job", b => + { + b.Property("JobId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.PrimitiveCollection("DependsOnJobsIds") + .HasMaxLength(64) + .HasColumnType("text[]"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("JobId1") + .HasColumnType("character varying(64)"); + + b.Property("JobType") + .HasColumnType("smallint"); + + b.Property("LastExecution") + .HasColumnType("timestamp with time zone"); + + b.Property("ParentJobId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RecurrenceMs") + .HasColumnType("numeric(20,0)"); + + b.Property("state") + .HasColumnType("smallint"); + + b.HasKey("JobId"); + + b.HasIndex("JobId1"); + + b.HasIndex("ParentJobId"); + + b.ToTable("Jobs"); + + b.HasDiscriminator("JobType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.LibraryConnectors.LibraryConnector", b => + { + b.Property("LibraryConnectorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("LibraryType") + .HasColumnType("smallint"); + + b.HasKey("LibraryConnectorId"); + + b.ToTable("LibraryConnectors"); + + b.HasDiscriminator("LibraryType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.Link", b => + { + b.Property("LinkId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("LinkProvider") + .IsRequired() + .HasColumnType("text"); + + b.Property("LinkUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.HasKey("LinkId"); + + b.HasIndex("MangaId"); + + b.ToTable("Link"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.Property("MangaId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ConnectorId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CoverFileNameInCache") + .HasColumnType("text"); + + b.Property("CoverUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("FolderName") + .IsRequired() + .HasColumnType("text"); + + b.Property("IgnoreChapterBefore") + .HasColumnType("real"); + + b.Property("MangaConnectorId") + .IsRequired() + .HasColumnType("character varying(32)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OriginalLanguage") + .HasColumnType("text"); + + b.Property("ReleaseStatus") + .HasColumnType("smallint"); + + b.Property("WebsiteUrl") + .IsRequired() + .HasColumnType("text"); + + b.Property("Year") + .HasColumnType("bigint"); + + b.HasKey("MangaId"); + + b.HasIndex("MangaConnectorId"); + + b.ToTable("Manga"); + }); + + modelBuilder.Entity("API.Schema.MangaAltTitle", b => + { + b.Property("AltTitleId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("AltTitleId"); + + b.HasIndex("MangaId"); + + b.ToTable("AltTitles"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaConnector", b => + { + b.Property("Name") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.PrimitiveCollection("BaseUris") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.PrimitiveCollection("SupportedLanguages") + .IsRequired() + .HasColumnType("text[]"); + + b.HasKey("Name"); + + b.ToTable("MangaConnectors"); + + b.HasDiscriminator("Name").HasValue("MangaConnector"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("API.Schema.MangaTag", b => + { + b.Property("Tag") + .HasColumnType("text"); + + b.HasKey("Tag"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("API.Schema.Notification", b => + { + b.Property("NotificationId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Urgency") + .HasColumnType("smallint"); + + b.HasKey("NotificationId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => + { + b.Property("Name") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property>("Headers") + .IsRequired() + .HasColumnType("hstore"); + + b.Property("HttpMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Name"); + + b.ToTable("NotificationConnectors"); + }); + + modelBuilder.Entity("AuthorManga", b => + { + b.Property("AuthorsAuthorId") + .HasColumnType("character varying(64)"); + + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.HasKey("AuthorsAuthorId", "MangaId"); + + b.HasIndex("MangaId"); + + b.ToTable("AuthorManga"); + }); + + modelBuilder.Entity("MangaMangaTag", b => + { + b.Property("MangaId") + .HasColumnType("character varying(64)"); + + b.Property("TagsTag") + .HasColumnType("text"); + + b.HasKey("MangaId", "TagsTag"); + + b.HasIndex("TagsTag"); + + b.ToTable("MangaMangaTag"); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("MangaId"); + + b.ToTable("Jobs", t => + { + t.Property("MangaId") + .HasColumnName("DownloadMangaCoverJob_MangaId"); + }); + + b.HasDiscriminator().HasValue((byte)4); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("MangaId"); + + b.HasDiscriminator().HasValue((byte)1); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("ChapterId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("ChapterId"); + + b.HasDiscriminator().HasValue((byte)0); + }); + + modelBuilder.Entity("API.Schema.Jobs.MoveFileOrFolderJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("FromLocation") + .IsRequired() + .HasColumnType("text"); + + b.Property("ToLocation") + .IsRequired() + .HasColumnType("text"); + + b.HasDiscriminator().HasValue((byte)3); + }); + + modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b => + { + b.HasBaseType("API.Schema.Jobs.Job"); + + b.Property("MangaId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasIndex("MangaId"); + + b.ToTable("Jobs", t => + { + t.Property("MangaId") + .HasColumnName("UpdateMetadataJob_MangaId"); + }); + + b.HasDiscriminator().HasValue((byte)2); + }); + + modelBuilder.Entity("API.Schema.LibraryConnectors.Kavita", b => + { + b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); + + b.HasDiscriminator().HasValue((byte)1); + }); + + modelBuilder.Entity("API.Schema.LibraryConnectors.Komga", b => + { + b.HasBaseType("API.Schema.LibraryConnectors.LibraryConnector"); + + b.HasDiscriminator().HasValue((byte)0); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.AsuraToon", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("AsuraToon"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.Bato", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Bato"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaDex", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaDex"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaHere", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaHere"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.MangaKatana", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("MangaKatana"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.Mangaworld", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Mangaworld"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.ManhuaPlus", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("ManhuaPlus"); + }); + + modelBuilder.Entity("API.Schema.MangaConnectors.Weebcentral", b => + { + b.HasBaseType("API.Schema.MangaConnectors.MangaConnector"); + + b.HasDiscriminator().HasValue("Weebcentral"); + }); + + modelBuilder.Entity("API.Schema.Chapter", b => + { + b.HasOne("API.Schema.Manga", "ParentManga") + .WithMany() + .HasForeignKey("ParentMangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ParentManga"); + }); + + modelBuilder.Entity("API.Schema.Jobs.Job", b => + { + b.HasOne("API.Schema.Jobs.Job", null) + .WithMany("DependsOnJobs") + .HasForeignKey("JobId1"); + + b.HasOne("API.Schema.Jobs.Job", "ParentJob") + .WithMany() + .HasForeignKey("ParentJobId"); + + b.Navigation("ParentJob"); + }); + + modelBuilder.Entity("API.Schema.Link", b => + { + b.HasOne("API.Schema.Manga", null) + .WithMany("Links") + .HasForeignKey("MangaId"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.HasOne("API.Schema.MangaConnectors.MangaConnector", "MangaConnector") + .WithMany() + .HasForeignKey("MangaConnectorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MangaConnector"); + }); + + modelBuilder.Entity("API.Schema.MangaAltTitle", b => + { + b.HasOne("API.Schema.Manga", null) + .WithMany("AltTitles") + .HasForeignKey("MangaId"); + }); + + modelBuilder.Entity("AuthorManga", b => + { + b.HasOne("API.Schema.Author", null) + .WithMany() + .HasForeignKey("AuthorsAuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.Manga", null) + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("MangaMangaTag", b => + { + b.HasOne("API.Schema.Manga", null) + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Schema.MangaTag", null) + .WithMany() + .HasForeignKey("TagsTag") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadMangaCoverJob", b => + { + b.HasOne("API.Schema.Manga", "Manga") + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Manga"); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadNewChaptersJob", b => + { + b.HasOne("API.Schema.Manga", "Manga") + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Manga"); + }); + + modelBuilder.Entity("API.Schema.Jobs.DownloadSingleChapterJob", b => + { + b.HasOne("API.Schema.Chapter", "Chapter") + .WithMany() + .HasForeignKey("ChapterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Chapter"); + }); + + modelBuilder.Entity("API.Schema.Jobs.UpdateMetadataJob", b => + { + b.HasOne("API.Schema.Manga", "Manga") + .WithMany() + .HasForeignKey("MangaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Manga"); + }); + + modelBuilder.Entity("API.Schema.Jobs.Job", b => + { + b.Navigation("DependsOnJobs"); + }); + + modelBuilder.Entity("API.Schema.Manga", b => + { + b.Navigation("AltTitles"); + + b.Navigation("Links"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Migrations/20250307143442_dev-070325-5.cs b/API/Migrations/20250307143442_dev-070325-5.cs new file mode 100644 index 0000000..21fba0d --- /dev/null +++ b/API/Migrations/20250307143442_dev-070325-5.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Migrations +{ + /// + public partial class dev0703255 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_NotificationConnectors", + table: "NotificationConnectors"); + + migrationBuilder.DropColumn( + name: "NotificationConnectorId", + table: "NotificationConnectors"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "NotificationConnectors", + type: "character varying(64)", + maxLength: 64, + nullable: false, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AddPrimaryKey( + name: "PK_NotificationConnectors", + table: "NotificationConnectors", + column: "Name"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_NotificationConnectors", + table: "NotificationConnectors"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "NotificationConnectors", + type: "text", + nullable: false, + oldClrType: typeof(string), + oldType: "character varying(64)", + oldMaxLength: 64); + + migrationBuilder.AddColumn( + name: "NotificationConnectorId", + table: "NotificationConnectors", + type: "character varying(64)", + maxLength: 64, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddPrimaryKey( + name: "PK_NotificationConnectors", + table: "NotificationConnectors", + column: "NotificationConnectorId"); + } + } +} diff --git a/API/Migrations/PgsqlContextModelSnapshot.cs b/API/Migrations/PgsqlContextModelSnapshot.cs index 6020108..f6956a4 100644 --- a/API/Migrations/PgsqlContextModelSnapshot.cs +++ b/API/Migrations/PgsqlContextModelSnapshot.cs @@ -1,5 +1,6 @@ // using System; +using System.Collections.Generic; using API.Schema; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -20,6 +21,7 @@ namespace API.Migrations .HasAnnotation("ProductVersion", "9.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 63); + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("API.Schema.Author", b => @@ -316,20 +318,29 @@ namespace API.Migrations modelBuilder.Entity("API.Schema.NotificationConnectors.NotificationConnector", b => { - b.Property("NotificationConnectorId") + b.Property("Name") .HasMaxLength(64) .HasColumnType("character varying(64)"); - b.Property("NotificationConnectorType") - .HasColumnType("smallint"); + b.Property("Body") + .IsRequired() + .HasColumnType("text"); - b.HasKey("NotificationConnectorId"); + b.Property>("Headers") + .IsRequired() + .HasColumnType("hstore"); + + b.Property("HttpMethod") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Name"); b.ToTable("NotificationConnectors"); - - b.HasDiscriminator("NotificationConnectorType"); - - b.UseTphMappingStrategy(); }); modelBuilder.Entity("AuthorManga", b => @@ -515,57 +526,6 @@ namespace API.Migrations b.HasDiscriminator().HasValue("Weebcentral"); }); - modelBuilder.Entity("API.Schema.NotificationConnectors.Gotify", b => - { - b.HasBaseType("API.Schema.NotificationConnectors.NotificationConnector"); - - b.Property("AppToken") - .IsRequired() - .HasColumnType("text"); - - b.Property("Endpoint") - .IsRequired() - .HasColumnType("text"); - - b.HasDiscriminator().HasValue((byte)0); - }); - - modelBuilder.Entity("API.Schema.NotificationConnectors.Lunasea", b => - { - b.HasBaseType("API.Schema.NotificationConnectors.NotificationConnector"); - - b.Property("Id") - .IsRequired() - .HasColumnType("text"); - - b.HasDiscriminator().HasValue((byte)1); - }); - - modelBuilder.Entity("API.Schema.NotificationConnectors.Ntfy", b => - { - b.HasBaseType("API.Schema.NotificationConnectors.NotificationConnector"); - - b.Property("Auth") - .IsRequired() - .HasColumnType("text"); - - b.Property("Endpoint") - .IsRequired() - .HasColumnType("text"); - - b.Property("Topic") - .IsRequired() - .HasColumnType("text"); - - b.ToTable("NotificationConnectors", t => - { - t.Property("Endpoint") - .HasColumnName("Ntfy_Endpoint"); - }); - - b.HasDiscriminator().HasValue((byte)2); - }); - modelBuilder.Entity("API.Schema.Chapter", b => { b.HasOne("API.Schema.Manga", "ParentManga") diff --git a/API/Schema/NotificationConnectors/Gotify.cs b/API/Schema/NotificationConnectors/Gotify.cs deleted file mode 100644 index ea1e6cc..0000000 --- a/API/Schema/NotificationConnectors/Gotify.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Text; -using Newtonsoft.Json; - -namespace API.Schema.NotificationConnectors; - -public class Gotify(string endpoint, string appToken) - : NotificationConnector(TokenGen.CreateToken(typeof(Gotify), endpoint), NotificationConnectorType.Gotify) -{ - public string Endpoint { get; init; } = endpoint; - public string AppToken { get; init; } = appToken; - - public override void SendNotification(string title, string notificationText) - { - MessageData message = new(title, notificationText); - HttpRequestMessage request = new(HttpMethod.Post, $"{endpoint}/message"); - request.Headers.Add("X-Gotify-Key", this.AppToken); - request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json"); - HttpResponseMessage response = _client.Send(request); - if (!response.IsSuccessStatusCode) - { - StreamReader sr = new (response.Content.ReadAsStream()); - //TODO - } - } - - private class MessageData - { - // ReSharper disable four times UnusedAutoPropertyAccessor.Local - public string message { get; } - public long priority { get; } - public string title { get; } - public Dictionary extras { get; } - - public MessageData(string title, string message) - { - this.title = title; - this.message = message; - this.extras = new(); - this.priority = 4; - } - } -} \ No newline at end of file diff --git a/API/Schema/NotificationConnectors/Lunasea.cs b/API/Schema/NotificationConnectors/Lunasea.cs deleted file mode 100644 index da5cf1b..0000000 --- a/API/Schema/NotificationConnectors/Lunasea.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Text; -using Newtonsoft.Json; - -namespace API.Schema.NotificationConnectors; - -public class Lunasea(string id) - : NotificationConnector(TokenGen.CreateToken(typeof(Lunasea), id), NotificationConnectorType.LunaSea) -{ - public string Id { get; init; } = id; - public override void SendNotification(string title, string notificationText) - { - MessageData message = new(title, notificationText); - HttpRequestMessage request = new(HttpMethod.Post, $"https://notify.lunasea.app/v1/custom/{id}"); - request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json"); - HttpResponseMessage response = _client.Send(request); - if (!response.IsSuccessStatusCode) - { - StreamReader sr = new (response.Content.ReadAsStream()); - //TODO - } - } - - private class MessageData - { - // ReSharper disable twice UnusedAutoPropertyAccessor.Local - public string title { get; } - public string body { get; } - - public MessageData(string title, string body) - { - this.title = title; - this.body = body; - } - } -} \ No newline at end of file diff --git a/API/Schema/NotificationConnectors/NotificationConnector.cs b/API/Schema/NotificationConnectors/NotificationConnector.cs index 0c29670..b83a83e 100644 --- a/API/Schema/NotificationConnectors/NotificationConnector.cs +++ b/API/Schema/NotificationConnectors/NotificationConnector.cs @@ -1,20 +1,65 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Text; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; namespace API.Schema.NotificationConnectors; -[PrimaryKey("NotificationConnectorId")] -public abstract class NotificationConnector(string notificationConnectorId, NotificationConnectorType notificationConnectorType) +[PrimaryKey("Name")] +public class NotificationConnector(string name, string url, Dictionary headers, string httpMethod, string body) { [MaxLength(64)] - public string NotificationConnectorId { get; } = notificationConnectorId; - public NotificationConnectorType NotificationConnectorType { get; init; } = notificationConnectorType; + public string Name { get; init; } = name; + public string Url { get; internal set; } = url; + + public Dictionary Headers { get; internal set; } = headers; + + public string HttpMethod { get; internal set; } = httpMethod; + + public string Body { get; internal set; } = body; + [JsonIgnore] [NotMapped] - protected readonly HttpClient _client = new(); - - public abstract void SendNotification(string title, string notificationText); + private readonly HttpClient Client = new() + { + DefaultRequestHeaders = { { "User-Agent", TrangaSettings.userAgent } } + }; + + public void SendNotification(string title, string notificationText) + { + CustomWebhookFormatProvider formatProvider = new CustomWebhookFormatProvider(title, notificationText); + string formattedUrl = string.Format(formatProvider, Url); + string formattedBody = string.Format(formatProvider, Body, title, notificationText); + Dictionary formattedHeaders = Headers.ToDictionary(h => h.Key, + h => string.Format(formatProvider, h.Value, title, notificationText)); + + HttpRequestMessage request = new(System.Net.Http.HttpMethod.Parse(HttpMethod), formattedUrl); + foreach (var (key, value) in formattedHeaders) + request.Headers.Add(key, value); + request.Content = new StringContent(formattedBody); + + HttpResponseMessage response = Client.Send(request); + } + + private class CustomWebhookFormatProvider(string title, string text) : IFormatProvider + { + public object? GetFormat(Type? formatType) + { + return this; + } + + public string Format(string fmt, object arg, IFormatProvider provider) + { + if(arg.GetType() != typeof(string)) + return arg.ToString() ?? string.Empty; + + StringBuilder sb = new StringBuilder(fmt); + sb.Replace("%title", title); + sb.Replace("%text", text); + + return sb.ToString(); + } + } } \ No newline at end of file diff --git a/API/Schema/NotificationConnectors/NotificationConnectorType.cs b/API/Schema/NotificationConnectors/NotificationConnectorType.cs deleted file mode 100644 index 9f38779..0000000 --- a/API/Schema/NotificationConnectors/NotificationConnectorType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace API.Schema.NotificationConnectors; - - -public enum NotificationConnectorType : byte -{ - Gotify = 0, - LunaSea = 1, - Ntfy = 2 -} \ No newline at end of file diff --git a/API/Schema/NotificationConnectors/Ntfy.cs b/API/Schema/NotificationConnectors/Ntfy.cs deleted file mode 100644 index ae7d3fb..0000000 --- a/API/Schema/NotificationConnectors/Ntfy.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Text; -using System.Text.RegularExpressions; -using Newtonsoft.Json; - -namespace API.Schema.NotificationConnectors; - -public class Ntfy : NotificationConnector -{ - private NotificationConnector _notificationConnectorImplementation; - public string Endpoint { get; init; } - public string Auth { get; init; } - public string Topic { get; init; } - - public Ntfy(string endpoint, string auth, string topic): base(TokenGen.CreateToken(typeof(Ntfy), endpoint), NotificationConnectorType.Ntfy) - { - Endpoint = endpoint; - Auth = auth; - Topic = topic; - } - - public Ntfy(string endpoint, string username, string password, string? topic = null) : - this(EndpointAndTopicFromUrl(endpoint)[0], topic??EndpointAndTopicFromUrl(endpoint)[1], AuthFromUsernamePassword(username, password)) - { - - } - - private static string AuthFromUsernamePassword(string username, string password) - { - string authHeader = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); - string authParam = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader)).Replace("=",""); - return authParam; - } - - private static string[] EndpointAndTopicFromUrl(string url) - { - string[] ret = new string[2]; - Regex rootUriRex = new(@"(https?:\/\/[a-zA-Z0-9-\.]+\.[a-zA-Z0-9]+)(?:\/([a-zA-Z0-9-\.]+))?.*"); - Match match = rootUriRex.Match(url); - if(!match.Success) - throw new ArgumentException($"Error getting URI from provided endpoint-URI: {url}"); - - ret[0] = match.Groups[1].Value; - ret[1] = match.Groups[2].Success && match.Groups[2].Value.Length > 0 ? match.Groups[2].Value : "tranga"; - - return ret; - } - - public override void SendNotification(string title, string notificationText) - { - MessageData message = new(title, Topic, notificationText); - HttpRequestMessage request = new(HttpMethod.Post, $"{this.Endpoint}?auth={this.Auth}"); - request.Content = new StringContent(JsonConvert.SerializeObject(message, Formatting.None), Encoding.UTF8, "application/json"); - HttpResponseMessage response = _client.Send(request); - if (!response.IsSuccessStatusCode) - { - StreamReader sr = new (response.Content.ReadAsStream()); - //TODO - } - } - - private class MessageData - { - // ReSharper disable UnusedAutoPropertyAccessor.Local - public string topic { get; } - public string title { get; } - public string message { get; } - public int priority { get; } - - public MessageData(string title, string topic, string message) - { - this.topic = topic; - this.title = title; - this.message = message; - this.priority = 3; - } - } -} \ No newline at end of file diff --git a/API/Schema/PgsqlContext.cs b/API/Schema/PgsqlContext.cs index 3f4e959..dffbbcc 100644 --- a/API/Schema/PgsqlContext.cs +++ b/API/Schema/PgsqlContext.cs @@ -36,11 +36,6 @@ public class PgsqlContext(DbContextOptions options) : DbContext(op .HasDiscriminator(l => l.LibraryType) .HasValue(LibraryType.Komga) .HasValue(LibraryType.Kavita); - modelBuilder.Entity() - .HasDiscriminator(n => n.NotificationConnectorType) - .HasValue(NotificationConnectorType.Gotify) - .HasValue(NotificationConnectorType.Ntfy) - .HasValue(NotificationConnectorType.LunaSea); modelBuilder.Entity() .HasDiscriminator(j => j.JobType)