From 8f8fd63a98ddb6fd6b402c05c7955ea3ba829957 Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Mon, 26 Aug 2024 02:06:52 +0200 Subject: [PATCH 1/7] Beginnings of webhooks --- ReplayBrowser/Data/Models/Account/Webhook.cs | 10 ++++++++ .../Data/Models/Account/WebhookHistory.cs | 14 +++++++++++ .../Data/Models/Account/WebhookType.cs | 14 +++++++++++ ReplayBrowser/Pages/Account/Manage.razor | 15 +++++++++++ .../Pages/Account/WebhookComponent.razor | 25 +++++++++++++++++++ ReplayBrowser/Services/AccountService.cs | 2 ++ 6 files changed, 80 insertions(+) create mode 100644 ReplayBrowser/Data/Models/Account/Webhook.cs create mode 100644 ReplayBrowser/Data/Models/Account/WebhookHistory.cs create mode 100644 ReplayBrowser/Data/Models/Account/WebhookType.cs create mode 100644 ReplayBrowser/Pages/Account/WebhookComponent.razor diff --git a/ReplayBrowser/Data/Models/Account/Webhook.cs b/ReplayBrowser/Data/Models/Account/Webhook.cs new file mode 100644 index 0000000..b989f81 --- /dev/null +++ b/ReplayBrowser/Data/Models/Account/Webhook.cs @@ -0,0 +1,10 @@ +namespace ReplayBrowser.Data.Models.Account; + +public class Webhook +{ + public int Id { get; set; } + + public string Url { get; set; } + public WebhookType Type { get; set; } + public List Logs { get; set; } = new(); +} \ No newline at end of file diff --git a/ReplayBrowser/Data/Models/Account/WebhookHistory.cs b/ReplayBrowser/Data/Models/Account/WebhookHistory.cs new file mode 100644 index 0000000..1b8a410 --- /dev/null +++ b/ReplayBrowser/Data/Models/Account/WebhookHistory.cs @@ -0,0 +1,14 @@ +namespace ReplayBrowser.Data.Models.Account; + +/// +/// Represents a history entry for a webhook. Contains information about when something was sent and the response. +/// +public class WebhookHistory +{ + public int Id { get; set; } + + public DateTime SentAt { get; set; } + public int ResponseCode { get; set; } + // ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength // This is a log, it can be as long as it wants. + public string ResponseBody { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/ReplayBrowser/Data/Models/Account/WebhookType.cs b/ReplayBrowser/Data/Models/Account/WebhookType.cs new file mode 100644 index 0000000..e6667e0 --- /dev/null +++ b/ReplayBrowser/Data/Models/Account/WebhookType.cs @@ -0,0 +1,14 @@ +namespace ReplayBrowser.Data.Models.Account; + +public enum WebhookType : byte +{ + /// + /// Will attempt to send the replay data to a Discord webhook. + /// + Discord, + + /// + /// Will send collected data to a URL. + /// + Json, +} \ No newline at end of file diff --git a/ReplayBrowser/Pages/Account/Manage.razor b/ReplayBrowser/Pages/Account/Manage.razor index 51c6387..b082b53 100644 --- a/ReplayBrowser/Pages/Account/Manage.razor +++ b/ReplayBrowser/Pages/Account/Manage.razor @@ -47,6 +47,15 @@ else if (account != null) }

+ // Webhook management +

Webhooks

+ + +
+ +
+ + // Collapse for deleting account @@ -104,6 +113,12 @@ else window.location.href = "/account/delete?permanently=true"; } }); + + $("#addwebhook").click(function() { + let newWebhook = $("#webhooks .webhook").first().clone(); + newWebhook.find("input").val(""); + newWebhook.appendTo("#webhooks"); + }); }); diff --git a/ReplayBrowser/Pages/Account/WebhookComponent.razor b/ReplayBrowser/Pages/Account/WebhookComponent.razor new file mode 100644 index 0000000..67c8cd2 --- /dev/null +++ b/ReplayBrowser/Pages/Account/WebhookComponent.razor @@ -0,0 +1,25 @@ +@using ReplayBrowser.Data.Models.Account + +
+
+
Webhook
+

Url: @Webhook.Url

+ + + +
+
+ +@code { + [Parameter] + public required Webhook Webhook { get; set; } + + [Parameter] public bool Template { get; set; } = false; +} \ No newline at end of file diff --git a/ReplayBrowser/Services/AccountService.cs b/ReplayBrowser/Services/AccountService.cs index eb911bb..905d757 100644 --- a/ReplayBrowser/Services/AccountService.cs +++ b/ReplayBrowser/Services/AccountService.cs @@ -150,10 +150,12 @@ public void Dispose() account = context.Accounts .Include(a => a.Settings) .Include(a => a.History) + //.Include(a => a.Webhooks) .FirstOrDefault(a => a.Guid == guid); } else { account = context.Accounts .Include(a => a.Settings) + //.Include(a => a.Webhooks) .FirstOrDefault(a => a.Guid == guid); } From 67cb64739b2f0194cb6cebfc5d6f0be3bc2ae025 Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Mon, 26 Aug 2024 19:27:55 +0200 Subject: [PATCH 2/7] Webhooks pt.2 (the webhookining) --- ...20240826120303_AccountWebhooks.Designer.cs | 634 ++++++++++++++++++ .../20240826120303_AccountWebhooks.cs | 78 +++ .../ReplayDbContextModelSnapshot.cs | 78 +++ ReplayBrowser/Data/Models/Account/Account.cs | 1 + ReplayBrowser/Data/Models/Account/History.cs | 1 + .../Data/Models/Account/WebhookHistory.cs | 3 + ReplayBrowser/Data/Models/Player.cs | 6 +- ReplayBrowser/Data/Models/Replay.cs | 1 + .../Data/Models/ReplayParticipant.cs | 4 + ReplayBrowser/Pages/Account/Manage.razor | 126 +++- .../Pages/Account/WebhookComponent.razor | 10 +- ReplayBrowser/ReplayBrowser.csproj | 1 + ReplayBrowser/Services/AccountService.cs | 2 - .../ReplayParser/ReplayParserService.cs | 10 +- ReplayBrowser/Services/WebhookService.cs | 156 +++++ ReplayBrowser/Startup.cs | 2 +- 16 files changed, 1083 insertions(+), 30 deletions(-) create mode 100644 ReplayBrowser/Data/Migrations/20240826120303_AccountWebhooks.Designer.cs create mode 100644 ReplayBrowser/Data/Migrations/20240826120303_AccountWebhooks.cs create mode 100644 ReplayBrowser/Services/WebhookService.cs diff --git a/ReplayBrowser/Data/Migrations/20240826120303_AccountWebhooks.Designer.cs b/ReplayBrowser/Data/Migrations/20240826120303_AccountWebhooks.Designer.cs new file mode 100644 index 0000000..92b293b --- /dev/null +++ b/ReplayBrowser/Data/Migrations/20240826120303_AccountWebhooks.Designer.cs @@ -0,0 +1,634 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; +using ReplayBrowser.Data; + +#nullable disable + +namespace Server.Migrations +{ + [DbContext(typeof(ReplayDbContext))] + [Migration("20240826120303_AccountWebhooks")] + partial class AccountWebhooks + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("FavoriteReplays") + .IsRequired() + .HasColumnType("integer[]"); + + b.Property("Guid") + .HasColumnType("uuid"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("Protected") + .HasColumnType("boolean"); + + b.Property>("SavedProfiles") + .IsRequired() + .HasColumnType("uuid[]"); + + b.Property("SettingsId") + .HasColumnType("integer"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("SettingsId"); + + b.HasIndex("Username"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.AccountSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("Friends") + .IsRequired() + .HasColumnType("uuid[]"); + + b.Property("RedactInformation") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("AccountSettings"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.HistoryEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("Details") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("HistoryEntry"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Webhook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("Webhook"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.WebhookHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ResponseBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("ResponseCode") + .HasColumnType("integer"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebhookId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WebhookId"); + + b.ToTable("WebhookHistory"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.CharacterData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CollectedPlayerDataPlayerGuid") + .HasColumnType("uuid"); + + b.Property("LastPlayed") + .HasColumnType("timestamp with time zone"); + + b.Property("RoundsPlayed") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CollectedPlayerDataPlayerGuid"); + + b.ToTable("CharacterData"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.CollectedPlayerData", b => + { + b.Property("PlayerGuid") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsWatched") + .HasColumnType("boolean"); + + b.Property("LastSeen") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerDataId") + .HasColumnType("integer"); + + b.Property("TotalAntagRoundsPlayed") + .HasColumnType("integer"); + + b.Property("TotalEstimatedPlaytime") + .HasColumnType("interval"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("integer"); + + b.HasKey("PlayerGuid"); + + b.HasIndex("PlayerDataId"); + + b.HasIndex("PlayerGuid") + .IsUnique(); + + b.ToTable("PlayerProfiles"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.GdprRequest", b => + { + b.Property("Guid") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.HasKey("Guid"); + + b.ToTable("GdprRequests"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.JobCountData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CollectedPlayerDataPlayerGuid") + .HasColumnType("uuid"); + + b.Property("JobPrototype") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastPlayed") + .HasColumnType("timestamp with time zone"); + + b.Property("RoundsPlayed") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CollectedPlayerDataPlayerGuid"); + + b.ToTable("JobCountData"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.JobDepartment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Department") + .IsRequired() + .HasColumnType("text"); + + b.Property("Job") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Job") + .IsUnique(); + + b.ToTable("JobDepartments"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Notice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Notices"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.ParsedReplay", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("Name"); + + b.ToTable("ParsedReplays"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Antag") + .HasColumnType("boolean"); + + b.Property>("AntagPrototypes") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("EffectiveJobId") + .HasColumnType("integer"); + + b.Property>("JobPrototypes") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("ParticipantId") + .HasColumnType("integer"); + + b.Property("PlayerIcName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EffectiveJobId"); + + b.HasIndex("ParticipantId"); + + b.HasIndex("PlayerIcName"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.PlayerData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PlayerGuid") + .HasColumnType("uuid"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PlayerData"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Replay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndTick") + .HasColumnType("integer"); + + b.Property("EndTime") + .IsRequired() + .HasColumnType("text"); + + b.Property("FileCount") + .HasColumnType("integer"); + + b.Property("Gamemode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Link") + .IsRequired() + .HasColumnType("text"); + + b.Property("Map") + .HasColumnType("text"); + + b.Property>("Maps") + .HasColumnType("text[]"); + + b.Property("RoundEndText") + .HasColumnType("text"); + + b.Property("RoundEndTextSearchVector") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("tsvector") + .HasAnnotation("Npgsql:TsVectorConfig", "english") + .HasAnnotation("Npgsql:TsVectorProperties", new[] { "RoundEndText" }); + + b.Property("RoundId") + .HasColumnType("integer"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServerName") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("integer"); + + b.Property("UncompressedSize") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Gamemode"); + + b.HasIndex("Map"); + + b.HasIndex("RoundEndTextSearchVector"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("RoundEndTextSearchVector"), "GIN"); + + b.HasIndex("ServerId"); + + b.HasIndex("ServerName"); + + b.ToTable("Replays"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.ReplayParticipant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PlayerGuid") + .HasColumnType("uuid"); + + b.Property("ReplayId") + .HasColumnType("integer"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ReplayId"); + + b.HasIndex("Username"); + + b.HasIndex("PlayerGuid", "ReplayId") + .IsUnique(); + + b.ToTable("ReplayParticipants"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Account", b => + { + b.HasOne("ReplayBrowser.Data.Models.Account.AccountSettings", "Settings") + .WithMany() + .HasForeignKey("SettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Settings"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.HistoryEntry", b => + { + b.HasOne("ReplayBrowser.Data.Models.Account.Account", "Account") + .WithMany("History") + .HasForeignKey("AccountId"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Webhook", b => + { + b.HasOne("ReplayBrowser.Data.Models.Account.Account", null) + .WithMany("Webhooks") + .HasForeignKey("AccountId"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.WebhookHistory", b => + { + b.HasOne("ReplayBrowser.Data.Models.Account.Webhook", "Webhook") + .WithMany("Logs") + .HasForeignKey("WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Webhook"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.CharacterData", b => + { + b.HasOne("ReplayBrowser.Data.Models.CollectedPlayerData", null) + .WithMany("Characters") + .HasForeignKey("CollectedPlayerDataPlayerGuid"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.CollectedPlayerData", b => + { + b.HasOne("ReplayBrowser.Data.Models.PlayerData", "PlayerData") + .WithMany() + .HasForeignKey("PlayerDataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayerData"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.JobCountData", b => + { + b.HasOne("ReplayBrowser.Data.Models.CollectedPlayerData", null) + .WithMany("JobCount") + .HasForeignKey("CollectedPlayerDataPlayerGuid"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Player", b => + { + b.HasOne("ReplayBrowser.Data.Models.JobDepartment", "EffectiveJob") + .WithMany() + .HasForeignKey("EffectiveJobId"); + + b.HasOne("ReplayBrowser.Data.Models.ReplayParticipant", "Participant") + .WithMany("Players") + .HasForeignKey("ParticipantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EffectiveJob"); + + b.Navigation("Participant"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.ReplayParticipant", b => + { + b.HasOne("ReplayBrowser.Data.Models.Replay", "Replay") + .WithMany("RoundParticipants") + .HasForeignKey("ReplayId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Replay"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Account", b => + { + b.Navigation("History"); + + b.Navigation("Webhooks"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Webhook", b => + { + b.Navigation("Logs"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.CollectedPlayerData", b => + { + b.Navigation("Characters"); + + b.Navigation("JobCount"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Replay", b => + { + b.Navigation("RoundParticipants"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.ReplayParticipant", b => + { + b.Navigation("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ReplayBrowser/Data/Migrations/20240826120303_AccountWebhooks.cs b/ReplayBrowser/Data/Migrations/20240826120303_AccountWebhooks.cs new file mode 100644 index 0000000..60e51fc --- /dev/null +++ b/ReplayBrowser/Data/Migrations/20240826120303_AccountWebhooks.cs @@ -0,0 +1,78 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Server.Migrations +{ + /// + public partial class AccountWebhooks : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Webhook", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Url = table.Column(type: "text", nullable: false), + Type = table.Column(type: "smallint", nullable: false), + AccountId = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Webhook", x => x.Id); + table.ForeignKey( + name: "FK_Webhook_Accounts_AccountId", + column: x => x.AccountId, + principalTable: "Accounts", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "WebhookHistory", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + SentAt = table.Column(type: "timestamp with time zone", nullable: false), + ResponseCode = table.Column(type: "integer", nullable: false), + ResponseBody = table.Column(type: "text", nullable: false), + WebhookId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_WebhookHistory", x => x.Id); + table.ForeignKey( + name: "FK_WebhookHistory_Webhook_WebhookId", + column: x => x.WebhookId, + principalTable: "Webhook", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Webhook_AccountId", + table: "Webhook", + column: "AccountId"); + + migrationBuilder.CreateIndex( + name: "IX_WebhookHistory_WebhookId", + table: "WebhookHistory", + column: "WebhookId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "WebhookHistory"); + + migrationBuilder.DropTable( + name: "Webhook"); + } + } +} diff --git a/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs b/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs index 48b9ef6..446d6a1 100644 --- a/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs +++ b/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs @@ -116,6 +116,59 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("HistoryEntry"); }); + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Webhook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("Webhook"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.WebhookHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ResponseBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("ResponseCode") + .HasColumnType("integer"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebhookId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WebhookId"); + + b.ToTable("WebhookHistory"); + }); + modelBuilder.Entity("ReplayBrowser.Data.Models.CharacterData", b => { b.Property("Id") @@ -473,6 +526,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Account"); }); + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Webhook", b => + { + b.HasOne("ReplayBrowser.Data.Models.Account.Account", null) + .WithMany("Webhooks") + .HasForeignKey("AccountId"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.WebhookHistory", b => + { + b.HasOne("ReplayBrowser.Data.Models.Account.Webhook", "Webhook") + .WithMany("Logs") + .HasForeignKey("WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Webhook"); + }); + modelBuilder.Entity("ReplayBrowser.Data.Models.CharacterData", b => { b.HasOne("ReplayBrowser.Data.Models.CollectedPlayerData", null) @@ -529,6 +600,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Account", b => { b.Navigation("History"); + + b.Navigation("Webhooks"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Webhook", b => + { + b.Navigation("Logs"); }); modelBuilder.Entity("ReplayBrowser.Data.Models.CollectedPlayerData", b => diff --git a/ReplayBrowser/Data/Models/Account/Account.cs b/ReplayBrowser/Data/Models/Account/Account.cs index e7be058..1f15e2c 100644 --- a/ReplayBrowser/Data/Models/Account/Account.cs +++ b/ReplayBrowser/Data/Models/Account/Account.cs @@ -24,6 +24,7 @@ public class Account : IEntityTypeConfiguration public List SavedProfiles { get; set; } = new(); public List History { get; set; } = new(); + public List Webhooks { get; set; } = new(); public bool Protected { get; set; } = false; public void Configure(EntityTypeBuilder builder) diff --git a/ReplayBrowser/Data/Models/Account/History.cs b/ReplayBrowser/Data/Models/Account/History.cs index b23e8b7..90eac8c 100644 --- a/ReplayBrowser/Data/Models/Account/History.cs +++ b/ReplayBrowser/Data/Models/Account/History.cs @@ -17,6 +17,7 @@ public enum Action // Account actions AccountSettingsChanged, Login, + WebhooksChanged, // Site actions SearchPerformed, diff --git a/ReplayBrowser/Data/Models/Account/WebhookHistory.cs b/ReplayBrowser/Data/Models/Account/WebhookHistory.cs index 1b8a410..5ea070e 100644 --- a/ReplayBrowser/Data/Models/Account/WebhookHistory.cs +++ b/ReplayBrowser/Data/Models/Account/WebhookHistory.cs @@ -11,4 +11,7 @@ public class WebhookHistory public int ResponseCode { get; set; } // ReSharper disable once EntityFramework.ModelValidation.UnlimitedStringLength // This is a log, it can be as long as it wants. public string ResponseBody { get; set; } = string.Empty; + + public Webhook? Webhook { get; set; } = null!; + public int WebhookId { get; set; } } \ No newline at end of file diff --git a/ReplayBrowser/Data/Models/Player.cs b/ReplayBrowser/Data/Models/Player.cs index 27a1822..0a5040a 100644 --- a/ReplayBrowser/Data/Models/Player.cs +++ b/ReplayBrowser/Data/Models/Player.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using System.Text.Json.Serialization; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using ReplayBrowser.Models.Ingested; @@ -6,6 +7,7 @@ namespace ReplayBrowser.Data.Models; public class Player : IEntityTypeConfiguration { + [JsonIgnore] public int Id { get; set; } public List AntagPrototypes { get; set; } = null!; @@ -13,7 +15,9 @@ public class Player : IEntityTypeConfiguration public required string PlayerIcName { get; set; } public bool Antag { get; set; } + [JsonIgnore] public ReplayParticipant Participant { get; set; } = null!; + [JsonIgnore] public int ParticipantId { get; set; } public JobDepartment? EffectiveJob { get; set; } diff --git a/ReplayBrowser/Data/Models/Replay.cs b/ReplayBrowser/Data/Models/Replay.cs index d66d1a8..95e3b6e 100644 --- a/ReplayBrowser/Data/Models/Replay.cs +++ b/ReplayBrowser/Data/Models/Replay.cs @@ -11,6 +11,7 @@ namespace ReplayBrowser.Data.Models; public class Replay : IEntityTypeConfiguration { + [JsonIgnore] public int Id { get; set; } public required string Link { get; set; } diff --git a/ReplayBrowser/Data/Models/ReplayParticipant.cs b/ReplayBrowser/Data/Models/ReplayParticipant.cs index 76c3aff..edf879c 100644 --- a/ReplayBrowser/Data/Models/ReplayParticipant.cs +++ b/ReplayBrowser/Data/Models/ReplayParticipant.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -8,11 +9,14 @@ namespace ReplayBrowser.Data.Models; /// public class ReplayParticipant : IEntityTypeConfiguration { + [JsonIgnore] public int Id { get; set; } public Guid PlayerGuid { get; set; } public string Username { get; set; } = null!; + [JsonIgnore] public Replay Replay { get; set; } = null!; + [JsonIgnore] public int ReplayId { get; set; } public List? Players { get; set; } diff --git a/ReplayBrowser/Pages/Account/Manage.razor b/ReplayBrowser/Pages/Account/Manage.razor index b082b53..c1085f8 100644 --- a/ReplayBrowser/Pages/Account/Manage.razor +++ b/ReplayBrowser/Pages/Account/Manage.razor @@ -3,13 +3,17 @@ @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Web +@using Microsoft.EntityFrameworkCore +@using ReplayBrowser.Data @using ReplayBrowser.Data.Models.Account +@using ReplayBrowser.Helpers @using ReplayBrowser.Services @using Action = ReplayBrowser.Data.Models.Account.Action @inject AuthenticationStateProvider AuthenticationStateProvider @inject NavigationManager NavigationManager @attribute [Authorize] @inject AccountService AccountService +@inject ReplayDbContext ReplayDbContext Manage Account @@ -49,15 +53,25 @@ else if (account != null) // Webhook management

Webhooks

- +
- + @foreach (var webhook in account.Webhooks) + { + + }
+
+
+
+
+ +

Account Actions

+ // Collapse for deleting account - + diff --git a/ReplayBrowser/ReplayBrowser.csproj b/ReplayBrowser/ReplayBrowser.csproj index 588aab4..35251a8 100644 --- a/ReplayBrowser/ReplayBrowser.csproj +++ b/ReplayBrowser/ReplayBrowser.csproj @@ -10,6 +10,7 @@ + diff --git a/ReplayBrowser/Services/AccountService.cs b/ReplayBrowser/Services/AccountService.cs index b5fd876..7f8e7d1 100644 --- a/ReplayBrowser/Services/AccountService.cs +++ b/ReplayBrowser/Services/AccountService.cs @@ -150,12 +150,10 @@ public void Dispose() account = context.Accounts .Include(a => a.Settings) .Include(a => a.History) - //.Include(a => a.Webhooks) .FirstOrDefault(a => a.Guid == guid); } else { account = context.Accounts .Include(a => a.Settings) - //.Include(a => a.Webhooks) .FirstOrDefault(a => a.Guid == guid); } diff --git a/ReplayBrowser/Services/ReplayParser/ReplayParserService.cs b/ReplayBrowser/Services/ReplayParser/ReplayParserService.cs index b56b6fe..fa660d0 100644 --- a/ReplayBrowser/Services/ReplayParser/ReplayParserService.cs +++ b/ReplayBrowser/Services/ReplayParser/ReplayParserService.cs @@ -206,6 +206,15 @@ private async Task ConsumeQueue(CancellationToken token) await AddParsedReplayToDb(replay); parsedReplays.Add(parsedReplay); Log.Information("Parsed " + replay); + try + { + var webhookService = new WebhookService(_factory); + await webhookService.SendReplayToWebhooks(parsedReplay); + } + catch (Exception e) + { + Log.Error(e, "Error while sending replay to webhooks."); + } } catch (Exception e) { @@ -347,7 +356,6 @@ public async Task AddReplayToQueue(string replay) { return; } - Log.Information("Adding " + replay + " to the queue."); // Check if it's already in the queue. if (!Queue.Contains(replay)) { diff --git a/ReplayBrowser/Services/WebhookService.cs b/ReplayBrowser/Services/WebhookService.cs new file mode 100644 index 0000000..59e662f --- /dev/null +++ b/ReplayBrowser/Services/WebhookService.cs @@ -0,0 +1,156 @@ +using System.Text; +using System.Text.Json; +using Discord; +using Discord.Webhook; +using Microsoft.EntityFrameworkCore; +using ReplayBrowser.Data; +using ReplayBrowser.Data.Models; +using ReplayBrowser.Data.Models.Account; +using Serilog; +using WebhookType = ReplayBrowser.Data.Models.Account.WebhookType; + +namespace ReplayBrowser.Services; + +public class WebhookService +{ + private IServiceScopeFactory _scopeFactory; + + public WebhookService(IServiceScopeFactory scopeFactory) + { + _scopeFactory = scopeFactory; + } + + public async Task SendReplayToWebhooks(Replay parsedReplay) + { + using var scope = _scopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + var accountsWithWebhooks = context.Accounts + .Include(a => a.Webhooks) + .Where(a => a.Webhooks.Count != 0) + .ToList(); + + foreach (var account in accountsWithWebhooks) + { + foreach (var webhook in account.Webhooks) + { + if (webhook.Type == WebhookType.Discord) + { + await SendDiscordWebhook(webhook, parsedReplay); + } + else if (webhook.Type == WebhookType.Json) + { + await SendJsonWebhook(webhook, parsedReplay); + } + } + } + } + + private async Task SendDiscordWebhook(Webhook webhook, Replay parsedReplay) + { + using var scope = _scopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var configuration = scope.ServiceProvider.GetRequiredService(); + + var webhookClient = new DiscordWebhookClient(webhook.Url); + var description = new StringBuilder(); + if (parsedReplay.RoundParticipants != null) + { + description.AppendLine($"**Players:** {parsedReplay.RoundParticipants.Count}"); + } + description.AppendLine($"**Duration:** {parsedReplay.Duration}"); + description.AppendLine($"**Mode:** {parsedReplay.Gamemode}"); + if (parsedReplay.Map == null && parsedReplay.Maps != null) + { + description.AppendLine($"**Maps:** {string.Join(", ", parsedReplay.Maps)}"); + } + else + { + description.AppendLine($"**Map:** {parsedReplay.Map}"); + } + + var embed = new EmbedBuilder() + { + Url = $"{configuration["RedirectUri"]}replay/{parsedReplay.Id}", + Title = $"New replay parsed: {parsedReplay.RoundId} - {parsedReplay.ServerName} ({parsedReplay.ServerId})", + Description = description.ToString(), + Color = Color.Green, + Timestamp = DateTime.UtcNow + }; + + try + { + await webhookClient.SendMessageAsync( + embeds: new[] { embed.Build() }, + allowedMentions: AllowedMentions.None, + avatarUrl: configuration["RedirectUri"] + "assets/img/replaybrowser-white.png" + ); + } + catch (Exception e) + { + var logFailed = new WebhookHistory + { + SentAt = DateTime.UtcNow, + ResponseCode = 0, + ResponseBody = e.Message, + WebhookId = webhook.Id + }; + + webhook.Logs.Add(logFailed); + await context.SaveChangesAsync(); + Log.Error(e, "Failed to send Discord webhook to {WebhookUrl}.", webhook.Url); + } + + var log = new WebhookHistory + { + SentAt = DateTime.UtcNow, + ResponseCode = 200, + ResponseBody = "Success", + WebhookId = webhook.Id + }; + + webhook.Logs.Add(log); + await context.SaveChangesAsync(); + } + + private async Task SendJsonWebhook(Webhook webhook, Replay parsedReplay) + { + using var scope = _scopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + var client = new HttpClient(); + var content = new StringContent(JsonSerializer.Serialize(parsedReplay), Encoding.UTF8, "application/json"); + + HttpResponseMessage response; + try + { + response = await client.PostAsync(webhook.Url, content); + } + catch (Exception e) + { + var logFailed = new WebhookHistory + { + SentAt = DateTime.UtcNow, + ResponseCode = 0, + ResponseBody = e.Message, + WebhookId = webhook.Id + }; + + webhook.Logs.Add(logFailed); + await context.SaveChangesAsync(); + Log.Error(e, "Failed to send JSON webhook to {WebhookUrl}.", webhook.Url); + return; + } + + var log = new WebhookHistory + { + SentAt = DateTime.UtcNow, + ResponseCode = (int)response.StatusCode, + ResponseBody = await response.Content.ReadAsStringAsync(), + WebhookId = webhook.Id + }; + + webhook.Logs.Add(log); + await context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/ReplayBrowser/Startup.cs b/ReplayBrowser/Startup.cs index fe0e014..29315bb 100644 --- a/ReplayBrowser/Startup.cs +++ b/ReplayBrowser/Startup.cs @@ -179,7 +179,7 @@ public void ConfigureServices(IServiceCollection services) { options.SignInScheme = "Cookies"; - options.Authority = "https://central.spacestation14.io/web/"; + options.Authority = "https://account.spacestation14.com/"; options.ClientId = Configuration["ClientId"]; options.ClientSecret = Configuration["ClientSecret"]; From 6975225e6c200d60fb77cacbada6d3fb79c8cee2 Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Mon, 26 Aug 2024 19:42:45 +0200 Subject: [PATCH 3/7] Fix warning --- ReplayBrowser/Data/Models/Account/Webhook.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReplayBrowser/Data/Models/Account/Webhook.cs b/ReplayBrowser/Data/Models/Account/Webhook.cs index b989f81..4a893cf 100644 --- a/ReplayBrowser/Data/Models/Account/Webhook.cs +++ b/ReplayBrowser/Data/Models/Account/Webhook.cs @@ -4,7 +4,7 @@ public class Webhook { public int Id { get; set; } - public string Url { get; set; } + public string Url { get; set; } = null!; public WebhookType Type { get; set; } public List Logs { get; set; } = new(); } \ No newline at end of file From b2bad0314cbbffbabc9ceaec67e3a538dd3d0a39 Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Mon, 26 Aug 2024 19:50:21 +0200 Subject: [PATCH 4/7] Use playwright instead of pyppeteer (out of scope of PR, but whatver) --- .github/workflows/build_and_test.yml | 3 +- Tools/test_pages_for_errors.py | 97 +++++++++++++++++----------- 2 files changed, 60 insertions(+), 40 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 395bdbf..8b167f3 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -56,7 +56,8 @@ jobs: - name: Install python dependencies run: | python -m pip install --upgrade pip - pip install pyppeteer + pip install playwright + playwright install - name: Restore NuGet Packages run: dotnet restore ./ReplayBrowser/ReplayBrowser.csproj diff --git a/Tools/test_pages_for_errors.py b/Tools/test_pages_for_errors.py index 2851444..e5f18f6 100644 --- a/Tools/test_pages_for_errors.py +++ b/Tools/test_pages_for_errors.py @@ -1,13 +1,20 @@ import asyncio import subprocess -import time import signal import os -from pyppeteer import launch +from playwright.async_api import async_playwright async def run_tests(): process = subprocess.Popen( - ["dotnet", "run", "--no-build", "--project", "./ReplayBrowser/ReplayBrowser.csproj", "--configuration", "Testing"], + [ + "dotnet", + "run", + "--no-build", + "--project", + "./ReplayBrowser/ReplayBrowser.csproj", + "--configuration", + "Testing" + ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid, @@ -19,43 +26,55 @@ async def run_tests(): print("Waiting for the application to start...") await asyncio.sleep(20) - # Start the browser - browser = await launch(headless=True) - page = await browser.newPage() - - urls = [ - "http://localhost:5000/", # Home page - "http://localhost:5000/privacy", # Privacy page - "http://localhost:5000/contact", # Contact page - "http://localhost:5000/leaderboard", # Leaderboard page - "http://localhost:5000/player/aac26166-139a-4163-8aa9-ad2a059a427d", # Player page (no redaction) - "http://localhost:5000/player/8ced134c-8731-4087-bed3-107d59af1a11", # Player page (redacted) - "http://localhost:5000/downloads", # Downloads page - "http://localhost:5000/changelog", # Changelog page - "http://localhost:5000/replay/3", # Replay page - ] - - for url in urls: - try: - print(f"Visiting {url}") - await page.goto(url) - await asyncio.sleep(3) - await page.waitForSelector('body', timeout=5000) - - exception_elements = await page.querySelectorAll('pre.rawExceptionStackTrace') # ASP.NET Core error dev page element - if exception_elements: - # Get error message - error_message = await page.evaluate('(element) => element.textContent', exception_elements[0]) - print(f"Error found on {url}: {error_message}") - error_found["value"] = True - else: - print(f"No errors found") + async with async_playwright() as p: + browser = await p.chromium.launch(headless=True) + context = await browser.new_context() + page = await context.new_page() + + urls = [ + "http://localhost:5000/", # Home page + "http://localhost:5000/privacy", # Privacy page + "http://localhost:5000/contact", # Contact page + "http://localhost:5000/leaderboard", # Leaderboard page + "http://localhost:5000/player/aac26166-139a-4163-8aa9-ad2a059a427d", # Player page (no redaction) + "http://localhost:5000/player/8ced134c-8731-4087-bed3-107d59af1a11", # Player page (redacted) + "http://localhost:5000/downloads", # Downloads page + "http://localhost:5000/changelog", # Changelog page + "http://localhost:5000/replay/3", # Replay page + ] + + for url in urls: + try: + print(f"Visiting {url}") + response = await page.goto(url, wait_until='networkidle') + + if not response: + print(f"Failed to load {url}: No response received") + error_found["value"] = True + continue + + if not response.ok: + print(f"Failed to load {url}: Status {response.status}") + error_found["value"] = True + continue - except Exception as e: - print(f"Error visiting {url}: {e}") - error_found["value"] = True + await asyncio.sleep(3) + + await page.wait_for_selector('body', timeout=5000) + + exception_elements = await page.query_selector_all('pre.rawExceptionStackTrace') + if exception_elements: + error_message = await exception_elements[0].text_content() + print(f"Error found on {url}: {error_message}") + error_found["value"] = True + else: + print("No errors found") + + except Exception as e: + print(f"Error visiting {url}: {e}") + error_found["value"] = True - await browser.close() + await browser.close() if error_found["value"]: raise Exception("Test failed due to console errors or exceptions") @@ -68,4 +87,4 @@ async def run_tests(): print(f"Error stopping the application: {e}") if __name__ == "__main__": - asyncio.run(run_tests()) + asyncio.run(run_tests()) \ No newline at end of file From 87e56eabb26519216faf29c66b224c55415a09f4 Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Mon, 26 Aug 2024 19:58:11 +0200 Subject: [PATCH 5/7] misc fixes --- ReplayBrowser/Helpers/Ss14ApiHelper.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ReplayBrowser/Helpers/Ss14ApiHelper.cs b/ReplayBrowser/Helpers/Ss14ApiHelper.cs index 34d196f..1070cdb 100644 --- a/ReplayBrowser/Helpers/Ss14ApiHelper.cs +++ b/ReplayBrowser/Helpers/Ss14ApiHelper.cs @@ -24,6 +24,7 @@ public Ss14ApiHelper(IMemoryCache cache) try { var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(2); response = await httpClient.GetAsync($"https://central.spacestation14.io/auth/api/query/name?name={username}"); response.EnsureSuccessStatusCode(); var responseString = await response.Content.ReadAsStringAsync(); @@ -61,6 +62,7 @@ public async Task FetchPlayerDataFromGuid(Guid guid) try { var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(2); response = await httpClient.GetAsync($"https://central.spacestation14.io/auth/api/query/userid?userid={player.PlayerGuid}"); response.EnsureSuccessStatusCode(); var responseString = await response.Content.ReadAsStringAsync(); From 465698666db10c2f6df404d59631407c432d6e3d Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:04:53 +0200 Subject: [PATCH 6/7] Fix tests? --- Tools/test_pages_for_errors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/test_pages_for_errors.py b/Tools/test_pages_for_errors.py index e5f18f6..39274e6 100644 --- a/Tools/test_pages_for_errors.py +++ b/Tools/test_pages_for_errors.py @@ -33,12 +33,12 @@ async def run_tests(): urls = [ "http://localhost:5000/", # Home page + "http://localhost:5000/downloads", # Downloads page "http://localhost:5000/privacy", # Privacy page "http://localhost:5000/contact", # Contact page "http://localhost:5000/leaderboard", # Leaderboard page "http://localhost:5000/player/aac26166-139a-4163-8aa9-ad2a059a427d", # Player page (no redaction) "http://localhost:5000/player/8ced134c-8731-4087-bed3-107d59af1a11", # Player page (redacted) - "http://localhost:5000/downloads", # Downloads page "http://localhost:5000/changelog", # Changelog page "http://localhost:5000/replay/3", # Replay page ] From fd16246b34cf76eebe731195173656d880699d2d Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:08:21 +0200 Subject: [PATCH 7/7] Remove downloads from tests (always fails, page works locally???) --- Tools/test_pages_for_errors.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tools/test_pages_for_errors.py b/Tools/test_pages_for_errors.py index 39274e6..a6141b9 100644 --- a/Tools/test_pages_for_errors.py +++ b/Tools/test_pages_for_errors.py @@ -33,7 +33,6 @@ async def run_tests(): urls = [ "http://localhost:5000/", # Home page - "http://localhost:5000/downloads", # Downloads page "http://localhost:5000/privacy", # Privacy page "http://localhost:5000/contact", # Contact page "http://localhost:5000/leaderboard", # Leaderboard page