From fbb6d1a4095ca045f624e2beeaeaaa08e666a0ff Mon Sep 17 00:00:00 2001 From: Simon Diesenreiter Date: Sat, 24 May 2025 18:34:15 +0200 Subject: [PATCH] feat: save submission history, refs #7 --- song_of_the_day/Config/AppConfiguration.cs | 7 +- ...onal data for song submissions.Designer.cs | 145 ++++++++++++++++++ ...18_additional data for song submissions.cs | 29 ++++ .../Migrations/DataContextModelSnapshot.cs | 3 + song_of_the_day/Data/SongSuggestion.cs | 4 + song_of_the_day/Program.cs | 75 +++++++-- 6 files changed, 243 insertions(+), 20 deletions(-) create mode 100644 song_of_the_day/Data/Migrations/20250524160218_additional data for song submissions.Designer.cs create mode 100644 song_of_the_day/Data/Migrations/20250524160218_additional data for song submissions.cs diff --git a/song_of_the_day/Config/AppConfiguration.cs b/song_of_the_day/Config/AppConfiguration.cs index 707166c..d6b2828 100644 --- a/song_of_the_day/Config/AppConfiguration.cs +++ b/song_of_the_day/Config/AppConfiguration.cs @@ -16,14 +16,15 @@ public class AppConfiguration this.SignalGroupId = Environment.GetEnvironmentVariable("SIGNAL_GROUP_ID") ?? "group.Wmk1UTVQTnh0Sjd6a0xiOGhnTnMzZlNkc2p2Q3c0SXJiQkU2eDlNU0hyTT0="; this.WebUIBaseURL = Environment.GetEnvironmentVariable("WEB_BASE_URL") ?? "https://sotd.disi.dev/"; this.UseBotTag = bool.Parse(Environment.GetEnvironmentVariable("USE_BOT_TAG") ?? "true"); - this.AverageDaysBetweenRequests = int.Parse(Environment.GetEnvironmentVariable("AVERAGE_DAYS_BETWEEN_REQUESTS") ?? "2"); + this.DaysBetweenRequests = int.Parse(Environment.GetEnvironmentVariable("DAYS_BETWEEN_REQUESTS") ?? "2"); var managersGroupName = Environment.GetEnvironmentVariable("LDAP_ADMINGROUP") ?? "admins"; var userGroupName = Environment.GetEnvironmentVariable("LDAP_USERGROUP") ?? "everybody"; + var bindValue = Environment.GetEnvironmentVariable("LDAP_BIND"); this.LDAPConfig = new ConfigurationAD() { Username = Environment.GetEnvironmentVariable("LDAP_BIND") ?? "cn=admin,dc=disi,dc=dev", Password = Environment.GetEnvironmentVariable("LDAP_PASS") ?? "adminPass2022!", - Port = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LDAP_BIND")) ? int.Parse(Environment.GetEnvironmentVariable("LDAP_BIND")) : 389, + Port = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("LDAP_BIND")) ? int.Parse(bindValue ?? "389") : 389, LDAPserver = Environment.GetEnvironmentVariable("LDAP_URL") ?? "192.168.1.108", LDAPQueryBase = Environment.GetEnvironmentVariable("LDAP_BASE") ?? "dc=disi,dc=dev", LDAPUserQueryBase = Environment.GetEnvironmentVariable("LDAP_BASE") ?? "ou=people,dc=disi,dc=dev", @@ -87,7 +88,7 @@ public class AppConfiguration get; private set; } - public int AverageDaysBetweenRequests + public int DaysBetweenRequests { get; private set; } diff --git a/song_of_the_day/Data/Migrations/20250524160218_additional data for song submissions.Designer.cs b/song_of_the_day/Data/Migrations/20250524160218_additional data for song submissions.Designer.cs new file mode 100644 index 0000000..82fe4b7 --- /dev/null +++ b/song_of_the_day/Data/Migrations/20250524160218_additional data for song submissions.Designer.cs @@ -0,0 +1,145 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace song_of_the_day.DataMigrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250524160218_additional data for song submissions")] + partial class additionaldataforsongsubmissions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Song", b => + { + b.Property("SongId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SongId")); + + b.Property("Artist") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("SongId"); + + b.ToTable("Songs"); + }); + + modelBuilder.Entity("SongSuggestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("SongId") + .HasColumnType("integer"); + + b.Property("Submitted") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SongId"); + + b.HasIndex("UserId"); + + b.ToTable("SongSuggestions"); + }); + + modelBuilder.Entity("SuggestionHelper", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SuggestionHelpers"); + }); + + modelBuilder.Entity("User", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("UserId")); + + b.Property("AssociationInProgress") + .HasColumnType("boolean"); + + b.Property("IsIntroduced") + .HasColumnType("boolean"); + + b.Property("LdapUserName") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NickName") + .HasColumnType("text"); + + b.Property("SignalMemberId") + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("SongSuggestion", b => + { + b.HasOne("Song", "Song") + .WithMany() + .HasForeignKey("SongId"); + + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Song"); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/song_of_the_day/Data/Migrations/20250524160218_additional data for song submissions.cs b/song_of_the_day/Data/Migrations/20250524160218_additional data for song submissions.cs new file mode 100644 index 0000000..2561ef3 --- /dev/null +++ b/song_of_the_day/Data/Migrations/20250524160218_additional data for song submissions.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace song_of_the_day.DataMigrations +{ + /// + public partial class additionaldataforsongsubmissions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Submitted", + table: "SongSuggestions", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Submitted", + table: "SongSuggestions"); + } + } +} diff --git a/song_of_the_day/Data/Migrations/DataContextModelSnapshot.cs b/song_of_the_day/Data/Migrations/DataContextModelSnapshot.cs index 680993c..59cf79b 100644 --- a/song_of_the_day/Data/Migrations/DataContextModelSnapshot.cs +++ b/song_of_the_day/Data/Migrations/DataContextModelSnapshot.cs @@ -57,6 +57,9 @@ namespace song_of_the_day.DataMigrations b.Property("SongId") .HasColumnType("integer"); + b.Property("Submitted") + .HasColumnType("boolean"); + b.Property("UserId") .HasColumnType("integer"); diff --git a/song_of_the_day/Data/SongSuggestion.cs b/song_of_the_day/Data/SongSuggestion.cs index 68fb4d3..a618afc 100644 --- a/song_of_the_day/Data/SongSuggestion.cs +++ b/song_of_the_day/Data/SongSuggestion.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Microsoft.EntityFrameworkCore; public class SongSuggestion @@ -7,4 +8,7 @@ public class SongSuggestion public User? User { get; set; } public Song? Song { get; set; } public DateTime Date { get; set; } + public bool UserHasSubmitted { get; set; } + public required SuggestionHelper SuggestionHelper { get; set; } + public bool HasUsedSuggestion { get; set; } } \ No newline at end of file diff --git a/song_of_the_day/Program.cs b/song_of_the_day/Program.cs index ef1f08a..a2df2ff 100644 --- a/song_of_the_day/Program.cs +++ b/song_of_the_day/Program.cs @@ -31,7 +31,7 @@ userCheckTimer.OnOccurence += async (s, ea) => var needsSaving = false; foreach (var memberId in memberList) { - var foundUser = dci.Users.Where(u => u.SignalMemberId == memberId).SingleOrDefault(); + var foundUser = dci.Users?.Where(u => u.SignalMemberId == memberId).SingleOrDefault(); if (foundUser == null) { var newUserContact = await SignalIntegration.Instance.GetContactAsync(memberId); @@ -47,7 +47,7 @@ userCheckTimer.OnOccurence += async (s, ea) => LdapUserName = string.Empty, AssociationInProgress = false, }; - dci.Users.Add(newUser); + dci.Users?.Add(newUser); needsSaving = true; } } @@ -65,7 +65,13 @@ var userIntroTimer = new CronTimer("*/1 * * * *", "Europe/Vienna", includingSeco userIntroTimer.OnOccurence += async (s, ea) => { var dci = DataContext.Instance; - var introUsers = dci.Users.Where(u => !u.IsIntroduced); + var introUsers = dci.Users?.Where(u => !u.IsIntroduced); + if (introUsers == null) + { + await dci.DisposeAsync(); + return; + } + bool needsSaving = false; foreach (var user in introUsers) { @@ -87,34 +93,63 @@ Console.WriteLine("Setting up pick of the day timer"); var pickOfTheDayTimer = new CronTimer("0 8 * * *", "Europe/Vienna", includingSeconds: false); pickOfTheDayTimer.OnOccurence += async (s, ea) => { - var rand = new Random(); - var num = rand.NextInt64(); - var mod = num % AppConfiguration.Instance.AverageDaysBetweenRequests; + var dci = DataContext.Instance; - if (mod > 0) + var lastSong = dci.SongSuggestions?.LastOrDefault(); + + if (lastSong != null && lastSong.Date >= DateTime.Today.Subtract(TimeSpan.FromDays(AppConfiguration.Instance.DaysBetweenRequests))) { Console.WriteLine("Skipping pick of the day today!"); + await dci.DisposeAsync(); return; } - var dci = DataContext.Instance; + if (dci.Users == null || dci.SuggestionHelpers == null || dci.SongSuggestions == null) + { + Console.WriteLine("Unable to properly initialize DB context!"); + await dci.DisposeAsync(); + return; + } var luckyUser = await dci.Users.ElementAtAsync((new Random()).Next(await dci.Users.CountAsync())); + if (luckyUser == null) + { + Console.WriteLine("Unable to determine today's lucky user!"); + await dci.DisposeAsync(); + return; + } var userName = string.IsNullOrEmpty(luckyUser.NickName) ? luckyUser.Name : luckyUser.NickName; var suggestion = await dci.SuggestionHelpers.ElementAtAsync((new Random()).Next(await dci.SuggestionHelpers.CountAsync())); - await SignalIntegration.Instance.SendMessageToGroupAsync($"Today's chosen person to share a song is: **{userName}**"); - await SignalIntegration.Instance.SendMessageToGroupAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*"); - await SignalIntegration.Instance.SendMessageToUserAsync($"Congratulations, you have been chosen to share a song today!", luckyUser.SignalMemberId); - await SignalIntegration.Instance.SendMessageToUserAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*", luckyUser.SignalMemberId); - await SignalIntegration.Instance.SendMessageToUserAsync($"For now please just share your suggestion with the group - in the future I might ask you to share directly with me or via the website to help me keep track of past suggestions!", luckyUser.SignalMemberId); + var newSongSuggestion = new SongSuggestion() + { + User = luckyUser, + SuggestionHelper = suggestion, + UserHasSubmitted = false, + HasUsedSuggestion = false, + Date = DateTime.Today + }; + if (luckyUser.SignalMemberId is string signalId) + { + await dci.SongSuggestions.AddAsync(newSongSuggestion); + await dci.SaveChangesAsync(); + await SignalIntegration.Instance.SendMessageToGroupAsync($"Today's chosen person to share a song is: **{userName}**"); + await SignalIntegration.Instance.SendMessageToGroupAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*"); + await SignalIntegration.Instance.SendMessageToUserAsync($"Congratulations, you have been chosen to share a song today!", signalId); + await SignalIntegration.Instance.SendMessageToUserAsync($"Today's (optional) suggestion helper to help you pick a song is:\n\n**{suggestion.Title}**\n\n*{suggestion.Description}*", signalId); + await SignalIntegration.Instance.SendMessageToUserAsync($"For now please just share your suggestion with the group - in the future I might ask you to share directly with me or via the website to help me keep track of past suggestions!", luckyUser.SignalMemberId); + } + await dci.DisposeAsync(); }; pickOfTheDayTimer.Start(); var startUserAssociationProcess = async (User userToAssociate) => { - await SignalIntegration.Instance.SendMessageToUserAsync($"Hi, I see you are not associated with any website user yet.", userToAssociate.SignalMemberId); - await SignalIntegration.Instance.SendMessageToUserAsync($"If you haven't yet, please navigate to https://users.disi.dev to create a new account.", userToAssociate.SignalMemberId); - await SignalIntegration.Instance.SendMessageToUserAsync($"Once you have done so, go to https://sotd.disi.dev, login, navigate to \"Unclaimed Phone Numbers\" and click on the \"Claim\" button to start the claim process.", userToAssociate.SignalMemberId); - await SignalIntegration.Instance.SendMessageToUserAsync($"With a future update you will be required to submit songs via your user account - at that point you will be skipped during the selection process if you have not yet claimed your phone number!", userToAssociate.SignalMemberId); + if (userToAssociate.SignalMemberId is string signalId) + { + await SignalIntegration.Instance.SendMessageToUserAsync($"Hi, I see you are not associated with any website user yet.", signalId); + await SignalIntegration.Instance.SendMessageToUserAsync($"If you haven't yet, please navigate to https://users.disi.dev to create a new account.", signalId); + await SignalIntegration.Instance.SendMessageToUserAsync($"Once you have done so, go to https://sotd.disi.dev, login, navigate to \"Unclaimed Phone Numbers\" and click on the \"Claim\" button to start the claim process.", signalId); + await SignalIntegration.Instance.SendMessageToUserAsync($"With a future update you will be required to submit songs via your user account - at that point you will be skipped during the selection process if you have not yet claimed your phone number!", signalId); + } }; Console.WriteLine("Setting up LdapAssociation timer"); @@ -122,6 +157,12 @@ var ldapAssociationTimer = new CronTimer("*/10 * * * *", "Europe/Vienna", includ ldapAssociationTimer.OnOccurence += async (s, ea) => { var dci = DataContext.Instance; + if (dci.Users == null) + { + Console.WriteLine("Unable to properly initialize DB context!"); + await dci.DisposeAsync(); + return; + } var nonAssociatedUsers = dci.Users.Where(u => string.IsNullOrEmpty(u.LdapUserName) && !u.AssociationInProgress); var needsSaving = false; foreach (var user in nonAssociatedUsers)