diff --git a/KfChatDotNetBot/ApplicationDbContext.cs b/KfChatDotNetBot/ApplicationDbContext.cs index e56f1b0..baea14d 100644 --- a/KfChatDotNetBot/ApplicationDbContext.cs +++ b/KfChatDotNetBot/ApplicationDbContext.cs @@ -18,4 +18,5 @@ public class ApplicationDbContext : DbContext public DbSet TwitchViewCounts { get; set; } public DbSet ChipsggBets { get; set; } public DbSet Images { get; set; } + public DbSet UsersWhoWere { get; set; } } \ No newline at end of file diff --git a/KfChatDotNetBot/ChatBot.cs b/KfChatDotNetBot/ChatBot.cs index fc776e5..0eb2c7c 100644 --- a/KfChatDotNetBot/ChatBot.cs +++ b/KfChatDotNetBot/ChatBot.cs @@ -10,6 +10,8 @@ using KfChatDotNetWsClient; using KfChatDotNetWsClient.Models; using KfChatDotNetWsClient.Models.Events; using KfChatDotNetWsClient.Models.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.IO; using NLog; using Websocket.Client; @@ -243,6 +245,7 @@ public class ChatBot { _seenMessages.Add(new SeenMessageMetadataModel {MessageId = message.MessageId, LastEdited = message.MessageEditDate}); } + UpdateUserLastActivityAsync(message.Author.Id, WhoWasActivityType.Message).Wait(_cancellationToken); } if (InitialStartCooldown) InitialStartCooldown = false; @@ -379,6 +382,9 @@ public class ChatBot { db.Users.Add(new UserDbModel { KfId = user.Id, KfUsername = user.Username }); _logger.Debug("Adding user to DB"); + // Immediately add to DB so we can populate activity + db.SaveChanges(); + UpdateUserLastActivityAsync(user.Id, WhoWasActivityType.Join).Wait(_cancellationToken); continue; } // Detect a username change @@ -387,6 +393,8 @@ public class ChatBot _logger.Debug("Username has updated, updating DB"); userDb.KfUsername = user.Username; } + + UpdateUserLastActivityAsync(user.Id, WhoWasActivityType.Join).Wait(_cancellationToken); } db.SaveChanges(); @@ -401,6 +409,39 @@ public class ChatBot _logger.Info("GambaSesh is no longer present"); GambaSeshPresent = false; } + + foreach (var user in userIds) + { + UpdateUserLastActivityAsync(user, WhoWasActivityType.Part).Wait(_cancellationToken); + } + } + + private async Task UpdateUserLastActivityAsync(int kfId, WhoWasActivityType type) + { + await using var db = new ApplicationDbContext(); + var user = await db.Users.FirstOrDefaultAsync(u => u.KfId == kfId, _cancellationToken); + if (user == null) + { + _logger.Error($"Failed to find user with KfId = {kfId} for the purposes of updating their last activity"); + return; + } + + var activity = + await db.UsersWhoWere.FirstOrDefaultAsync(u => u.User == user && u.ActivityType == type, _cancellationToken); + if (activity == null) + { + await db.UsersWhoWere.AddAsync(new UserWhoWasDbModel + { + User = user, + FirstOccurence = DateTimeOffset.UtcNow, + ActivityType = type, + LatestOccurence = DateTimeOffset.UtcNow + }, _cancellationToken); + await db.SaveChangesAsync(_cancellationToken); + return; + } + activity.LatestOccurence = DateTimeOffset.UtcNow; + await db.SaveChangesAsync(_cancellationToken); } private void OnKfWsDisconnected(object sender, DisconnectionInfo disconnectionInfo) diff --git a/KfChatDotNetBot/Migrations/20250413180751_UserWhoWas.Designer.cs b/KfChatDotNetBot/Migrations/20250413180751_UserWhoWas.Designer.cs new file mode 100644 index 0000000..3e6f895 --- /dev/null +++ b/KfChatDotNetBot/Migrations/20250413180751_UserWhoWas.Designer.cs @@ -0,0 +1,328 @@ +// +using System; +using KfChatDotNetBot; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace KfChatDotNetBot.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250413180751_UserWhoWas")] + partial class UserWhoWas + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.1"); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.ChipsggBetDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("REAL"); + + b.Property("BetId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CurrencyPrice") + .HasColumnType("REAL"); + + b.Property("GameTitle") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Multiplier") + .HasColumnType("REAL"); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Win") + .HasColumnType("INTEGER"); + + b.Property("Winnings") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("ChipsggBets"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.HowlggBetsDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Bet") + .HasColumnType("INTEGER"); + + b.Property("BetId") + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Game") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GameId") + .HasColumnType("INTEGER"); + + b.Property("Profit") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("HowlggBets"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.ImageDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastSeen") + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.JuicerDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("REAL"); + + b.Property("JuicedAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Juicers"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.RainbetBetsDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BetId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("BetSeenAt") + .HasColumnType("TEXT"); + + b.Property("GameName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Multiplier") + .HasColumnType("REAL"); + + b.Property("Payout") + .HasColumnType("REAL"); + + b.Property("PublicId") + .HasColumnType("TEXT"); + + b.Property("RainbetUserId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("RainbetBets"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.SettingDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CacheDuration") + .HasColumnType("REAL"); + + b.Property("Default") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsSecret") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Regex") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.Property("ValueType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.TwitchViewCountDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServerTime") + .HasColumnType("REAL"); + + b.Property("Time") + .HasColumnType("TEXT"); + + b.Property("Topic") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Viewers") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("TwitchViewCounts"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.UserDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Ignored") + .HasColumnType("INTEGER"); + + b.Property("KfId") + .HasColumnType("INTEGER"); + + b.Property("KfUsername") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserRight") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.UserWhoWasDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActivityType") + .HasColumnType("INTEGER"); + + b.Property("FirstOccurence") + .HasColumnType("TEXT"); + + b.Property("LatestOccurence") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UsersWhoWere"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.JuicerDbModel", b => + { + b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.UserWhoWasDbModel", b => + { + b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/KfChatDotNetBot/Migrations/20250413180751_UserWhoWas.cs b/KfChatDotNetBot/Migrations/20250413180751_UserWhoWas.cs new file mode 100644 index 0000000..51ea9d8 --- /dev/null +++ b/KfChatDotNetBot/Migrations/20250413180751_UserWhoWas.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace KfChatDotNetBot.Migrations +{ + /// + public partial class UserWhoWas : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UsersWhoWere", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "INTEGER", nullable: false), + FirstOccurence = table.Column(type: "TEXT", nullable: false), + LatestOccurence = table.Column(type: "TEXT", nullable: false), + ActivityType = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UsersWhoWere", x => x.Id); + table.ForeignKey( + name: "FK_UsersWhoWere_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_UsersWhoWere_UserId", + table: "UsersWhoWere", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UsersWhoWere"); + } + } +} diff --git a/KfChatDotNetBot/Migrations/ApplicationDbContextModelSnapshot.cs b/KfChatDotNetBot/Migrations/ApplicationDbContextModelSnapshot.cs index 435393f..dd5cc95 100644 --- a/KfChatDotNetBot/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/KfChatDotNetBot/Migrations/ApplicationDbContextModelSnapshot.cs @@ -273,6 +273,31 @@ namespace KfChatDotNetBot.Migrations b.ToTable("Users"); }); + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.UserWhoWasDbModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActivityType") + .HasColumnType("INTEGER"); + + b.Property("FirstOccurence") + .HasColumnType("TEXT"); + + b.Property("LatestOccurence") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UsersWhoWere"); + }); + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.JuicerDbModel", b => { b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User") @@ -283,6 +308,17 @@ namespace KfChatDotNetBot.Migrations b.Navigation("User"); }); + + modelBuilder.Entity("KfChatDotNetBot.Models.DbModels.UserWhoWasDbModel", b => + { + b.HasOne("KfChatDotNetBot.Models.DbModels.UserDbModel", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); #pragma warning restore 612, 618 } } diff --git a/KfChatDotNetBot/Models/DbModels/UserDbModel.cs b/KfChatDotNetBot/Models/DbModels/UserDbModel.cs index a41a554..60458d6 100644 --- a/KfChatDotNetBot/Models/DbModels/UserDbModel.cs +++ b/KfChatDotNetBot/Models/DbModels/UserDbModel.cs @@ -19,4 +19,20 @@ public enum UserRight [Description("Rat")] Guest = 10, Loser = 0 +} + +public enum WhoWasActivityType +{ + Join, + Part, + Message +} + +public class UserWhoWasDbModel +{ + public int Id { get; set; } + public required UserDbModel User { get; set; } + public required DateTimeOffset FirstOccurence { get; set; } + public required DateTimeOffset LatestOccurence { get; set; } + public required WhoWasActivityType ActivityType { get; set; } } \ No newline at end of file