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
+ Add webhook
+
+
+
+
+
+
Save
// 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
+
+
+
Delete webhook
+
+
+
+@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
- Add webhook
+ Add webhook
-
+ @foreach (var webhook in account.Webhooks)
+ {
+
+ }
+
+
Save
+
+
+
+ Account Actions
+
// Collapse for deleting account
Delete Account
@@ -91,12 +105,29 @@ else
Account not found (how did you get here?)
}
+
+
@code {
@@ -130,7 +173,16 @@ else
protected override async Task OnInitializedAsync()
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
- account = await AccountService.GetAccount(authState);
+ var guid = AccountHelper.GetAccountGuid(authState);
+ if (guid == null)
+ {
+ ErrorMessage = "Account not found, please go to the home page and then try again. How did you get here?";
+ return;
+ }
+ account = await ReplayDbContext.Accounts
+ .Include(x => x.Webhooks)
+ .Include(a => a.Settings)
+ .FirstOrDefaultAsync(x => x.Guid == guid);
if (account == null)
{
@@ -149,6 +201,7 @@ else
return;
var redact = queryDictionary["redact"];
+ var webhooks = queryDictionary["webhooks"];
var changes = 0;
if (redact != null)
{
@@ -160,24 +213,57 @@ else
}
}
- if (changes == 0)
- return;
-
- try
+ if (webhooks != null)
{
- await AccountService.AddHistory(account, new HistoryEntry()
+ var valueWebhooks = JsonSerializer.Deserialize>(webhooks);
+ if (valueWebhooks != null)
{
- Action = Enum.GetName(typeof(Action), Action.AccountSettingsChanged) ?? "Unknown",
- Time = DateTime.UtcNow,
- Details = $"Old settings: {JsonSerializer.Serialize(oldAccountSettings)} New settings: {JsonSerializer.Serialize(account.Settings)}",
- });
+ //await ReplayDbContext.Database.ExecuteSqlRawAsync("DELETE FROM \"Webhook\" WHERE \"AccountId\" = {0}", account.Id);
+ //await AccountService.AddHistory(account, new HistoryEntry()
+ //{
+ // Action = Enum.GetName(typeof(Action), Action.WebhooksChanged) ?? "Unknown",
+ // Time = DateTime.UtcNow,
+ // Details = $"Old webhooks: {JsonSerializer.Serialize(account.Webhooks)} New webhooks: {JsonSerializer.Serialize(valueWebhooks)}",
+ //});
+ account.Webhooks.Clear();
+ await ReplayDbContext.SaveChangesAsync();
+
+ var webhookList = new List();
+ foreach (var webhook in valueWebhooks)
+ {
+ var url = webhook.GetProperty("url").GetString();
+ var type = webhook.GetProperty("type").GetString();
+ if (url == null || type == null)
+ continue;
+
+ var enumParsed = Enum.TryParse(type, out var webhookType);
+ if (!enumParsed)
+ continue;
+
+ webhookList.Add(new Webhook()
+ {
+ Url = url,
+ Type = webhookType,
+ });
+ }
+
+ account.Webhooks = webhookList;
- await AccountService.UpdateAccount(account);
- ChangeSaved = true;
+ changes++;
+ }
}
- catch (Exception e)
+
+ if (changes == 0)
+ return;
+
+ await AccountService.AddHistory(account, new HistoryEntry()
{
- ErrorMessage = e.Message + "\n " + e.StackTrace;
- }
+ Action = Enum.GetName(typeof(Action), Action.AccountSettingsChanged) ?? "Unknown",
+ Time = DateTime.UtcNow,
+ Details = $"Old settings: {JsonSerializer.Serialize(oldAccountSettings)} New settings: {JsonSerializer.Serialize(account.Settings)}",
+ });
+
+ await AccountService.UpdateAccount(account);
+ ChangeSaved = true;
}
}
\ No newline at end of file
diff --git a/ReplayBrowser/Pages/Account/WebhookComponent.razor b/ReplayBrowser/Pages/Account/WebhookComponent.razor
index 67c8cd2..f502e49 100644
--- a/ReplayBrowser/Pages/Account/WebhookComponent.razor
+++ b/ReplayBrowser/Pages/Account/WebhookComponent.razor
@@ -1,19 +1,19 @@
@using ReplayBrowser.Data.Models.Account
-
+
Webhook
-
Url: @Webhook.Url
-
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