diff --git a/.vscode/launch.json b/.vscode/launch.json index 1955653..0610636 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/bin/Debug/net8.0/vassago.dll", + "program": "${workspaceFolder}/bin/Debug/net9.0/vassago.dll", "args": [], "cwd": "${workspaceFolder}", "stopAtEntry": false, diff --git a/Behaver.cs b/Behaver.cs index c225422..859f6f7 100644 --- a/Behaver.cs +++ b/Behaver.cs @@ -8,6 +8,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Collections.Generic; +using vassago.ProtocolInterfaces.DiscordInterface; public class Behaver { @@ -72,55 +73,31 @@ public class Behaver public void MarkSelf(Account selfAccount) { - var db = new ChattingContext(); if(SelfUser == null) { SelfUser = selfAccount.IsUser; } else if (SelfUser != selfAccount.IsUser) { - CollapseUsers(SelfUser, selfAccount.IsUser, db); + CollapseUsers(SelfUser, selfAccount.IsUser); } - SelfAccounts = db.Accounts.Where(a => a.IsUser == SelfUser).ToList(); - db.SaveChanges(); + SelfAccounts = Rememberer.SearchAccounts(a => a.IsUser == SelfUser); + Rememberer.RememberAccount(selfAccount); } - public bool CollapseUsers(User primary, User secondary, ChattingContext db) + public bool CollapseUsers(User primary, User secondary) { - Console.WriteLine($"{secondary.Id} is being consumed into {primary.Id}"); - primary.Accounts.AddRange(secondary.Accounts); + if(primary.Accounts == null) + primary.Accounts = new List(); + if(secondary.Accounts != null) + primary.Accounts.AddRange(secondary.Accounts); foreach(var a in secondary.Accounts) { a.IsUser = primary; } secondary.Accounts.Clear(); - Console.WriteLine("accounts transferred"); - try - { - db.SaveChangesAsync().Wait(); - } - catch(Exception e) - { - Console.WriteLine("First save exception."); - Console.Error.WriteLine(e); - return false; - } - Console.WriteLine("saved"); - - - db.Users.Remove(secondary); - Console.WriteLine("old account cleaned up"); - try - { - db.SaveChangesAsync().Wait(); - } - catch(Exception e) - { - Console.WriteLine("Second save exception."); - Console.Error.WriteLine(e); - return false; - } - Console.WriteLine("saved, again, separately"); + Rememberer.ForgetUser(secondary); + Rememberer.RememberUser(primary); return true; } } diff --git a/Behavior/LinkMe.cs b/Behavior/LinkMe.cs index 841e6b6..fcd039b 100644 --- a/Behavior/LinkMe.cs +++ b/Behavior/LinkMe.cs @@ -72,7 +72,7 @@ public class LinkClose : Behavior return true; } - if(Behaver.Instance.CollapseUsers(_primary.IsUser, secondary, new ChattingContext())) + if(Behaver.Instance.CollapseUsers(_primary.IsUser, secondary)) { await message.Channel.SendMessage("done :)"); } diff --git a/ConsoleService.cs b/ConsoleService.cs index f4ba7e3..1a568a3 100644 --- a/ConsoleService.cs +++ b/ConsoleService.cs @@ -4,10 +4,11 @@ namespace vassago using vassago; using vassago.Models; using vassago.TwitchInterface; + using vassago.ProtocolInterfaces.DiscordInterface; + using System.Runtime.CompilerServices; internal class ConsoleService : IHostedService { - public ConsoleService(IConfiguration aspConfig) { Shared.DBConnectionString = aspConfig["DBConnectionString"]; @@ -21,14 +22,15 @@ namespace vassago public async Task StartAsync(CancellationToken cancellationToken) { + var initTasks = new List(); var dbc = new ChattingContext(); await dbc.Database.MigrateAsync(cancellationToken); if (DiscordTokens?.Any() ?? false) foreach (var dt in DiscordTokens) { - var d = new DiscordInterface.DiscordInterface(); - await d.Init(dt); + var d = new DiscordInterface(); + initTasks.Add(d.Init(dt)); ProtocolInterfaces.ProtocolList.discords.Add(d); } @@ -36,10 +38,11 @@ namespace vassago foreach (var tc in TwitchConfigs) { var t = new TwitchInterface.TwitchInterface(); - await t.Init(tc); + initTasks.Add(t.Init(tc)); ProtocolInterfaces.ProtocolList.twitchs.Add(t); } - Console.WriteLine("survived initting"); + + Task.WaitAll(initTasks, cancellationToken); } public Task StopAsync(CancellationToken cancellationToken) diff --git a/Models/Account.cs b/Models/Account.cs index bb3eaf3..7ca9aba 100644 --- a/Models/Account.cs +++ b/Models/Account.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Reflection; +using System.Text.Json.Serialization; using System.Threading.Tasks; public class Account @@ -27,5 +28,6 @@ public class Account public bool IsBot { get; set; } //webhook counts public Channel SeenInChannel { get; set; } public string Protocol { get; set; } + [JsonIgnore] public User IsUser {get; set;} } \ No newline at end of file diff --git a/Models/Channel.cs b/Models/Channel.cs index f9c820d..0026965 100644 --- a/Models/Channel.cs +++ b/Models/Channel.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Web; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; using static vassago.Models.Enumerations; public class Channel @@ -17,6 +18,7 @@ public class Channel public string DisplayName { get; set; } [DeleteBehavior(DeleteBehavior.Cascade)] public List SubChannels { get; set; } + [JsonIgnore] public Channel ParentChannel { get; set; } public string Protocol { get; set; } [DeleteBehavior(DeleteBehavior.Cascade)] @@ -82,6 +84,23 @@ public class Channel } } } + + /// + ///break self-referencing loops for library-agnostic serialization + /// + public Channel AsSerializable() + { + var toReturn = this.MemberwiseClone() as Channel; + toReturn.ParentChannel = null; + if(toReturn.Users?.Count > 0) + { + foreach (var account in toReturn.Users) + { + account.SeenInChannel = null; + } + } + return toReturn; + } } public class DefinitePermissionSettings diff --git a/Program.cs b/Program.cs index 7b5c9f0..6e8287e 100644 --- a/Program.cs +++ b/Program.cs @@ -11,7 +11,10 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); builder.Services.AddSingleton(); builder.Services.AddDbContext(); -builder.Services.AddControllers().AddNewtonsoftJson(); +builder.Services.AddControllers().AddNewtonsoftJson(options => { + options.SerializerSettings.ReferenceLoopHandling = + Newtonsoft.Json.ReferenceLoopHandling.Ignore; + }); builder.Services.AddProblemDetails(); builder.Services.Configure(o => { o.ViewLocationFormats.Clear(); diff --git a/ProtocolInterfaces/DiscordInterface/DiscordInterface.cs b/ProtocolInterfaces/DiscordInterface/DiscordInterface.cs index 6234548..5dc6e9e 100644 --- a/ProtocolInterfaces/DiscordInterface/DiscordInterface.cs +++ b/ProtocolInterfaces/DiscordInterface/DiscordInterface.cs @@ -13,22 +13,16 @@ using Microsoft.EntityFrameworkCore; using System.Threading; using System.Reactive.Linq; -namespace vassago.DiscordInterface; +namespace vassago.ProtocolInterfaces.DiscordInterface; public class DiscordInterface { internal const string PROTOCOL = "discord"; internal DiscordSocketClient client; private bool eventsSignedUp = false; - private ChattingContext _db; - private static SemaphoreSlim discordChannelSetup = new SemaphoreSlim(1, 1); + private static readonly SemaphoreSlim discordChannelSetup = new(1, 1); private Channel protocolAsChannel; - public DiscordInterface() - { - _db = new ChattingContext(); - } - public async Task Init(string token) { await SetupDiscordChannel(); @@ -39,8 +33,8 @@ public class DiscordInterface Console.WriteLine(msg.ToString()); return Task.CompletedTask; }; - client.Connected += SelfConnected; - client.Ready += ClientReady; + client.Connected += () => Task.Run(SelfConnected); + client.Ready += () => Task.Run(ClientReady); await client.LoginAsync(TokenType.Bot, token); await client.StartAsync(); @@ -52,7 +46,7 @@ public class DiscordInterface try { - protocolAsChannel = _db.Channels.FirstOrDefault(c => c.ParentChannel == null && c.Protocol == PROTOCOL); + protocolAsChannel = Rememberer.SearchChannel(c => c.ParentChannel == null && c.Protocol == PROTOCOL); if (protocolAsChannel == null) { protocolAsChannel = new Channel() @@ -66,13 +60,18 @@ public class DiscordInterface ReactionsPossible = true, ExternalId = null, Protocol = PROTOCOL, - SubChannels = new List() + SubChannels = [] }; - protocolAsChannel.SendMessage = (t) => { throw new InvalidOperationException($"discord itself cannot accept text"); }; - protocolAsChannel.SendFile = (f, t) => { throw new InvalidOperationException($"discord itself cannot send file"); }; - _db.Channels.Add(protocolAsChannel); - _db.SaveChanges(); } + else + { + Console.WriteLine($"discord, channel with id {protocolAsChannel.Id}, already exists"); + } + protocolAsChannel.DisplayName = "discord (itself)"; + protocolAsChannel.SendMessage = (t) => { throw new InvalidOperationException($"protocol isn't a real channel, cannot accept text"); }; + protocolAsChannel.SendFile = (f, t) => { throw new InvalidOperationException($"protocol isn't a real channel, cannot send file"); }; + protocolAsChannel = Rememberer.RememberChannel(protocolAsChannel); + Console.WriteLine($"protocol as channel addeed; {protocolAsChannel}"); } finally { @@ -89,7 +88,7 @@ public class DiscordInterface client.MessageReceived += MessageReceived; // _client.MessageUpdated += - //client.UserJoined += UserJoined; + client.UserJoined += UserJoined; client.SlashCommandExecuted += SlashCommandHandler; //client.ChannelCreated += // _client.ChannelDestroyed += @@ -114,21 +113,29 @@ public class DiscordInterface private async Task SelfConnected() { - var selfAccount = UpsertAccount(client.CurrentUser, protocolAsChannel); - selfAccount.DisplayName = client.CurrentUser.Username; - await _db.SaveChangesAsync(); + await discordChannelSetup.WaitAsync(); - Behaver.Instance.MarkSelf(selfAccount); + try + { + var selfAccount = UpsertAccount(client.CurrentUser, protocolAsChannel); + selfAccount.DisplayName = client.CurrentUser.Username; + Behaver.Instance.MarkSelf(selfAccount); + } + finally + { + discordChannelSetup.Release(); + } } private async Task MessageReceived(SocketMessage messageParam) { - var suMessage = messageParam as SocketUserMessage; - if (suMessage == null) + if (messageParam is not SocketUserMessage) { Console.WriteLine($"{messageParam.Content}, but not a user message"); return; } + var suMessage = messageParam as SocketUserMessage; + Console.WriteLine($"#{suMessage.Channel}[{DateTime.Now}][{suMessage.Author.Username} [id={suMessage.Author.Id}]][msg id: {suMessage.Id}] {suMessage.Content}"); var m = UpsertMessage(suMessage); @@ -140,26 +147,16 @@ public class DiscordInterface } await Behaver.Instance.ActOn(m); m.ActedOn = true; // for its own ruposess it might act on it later, but either way, fuck it, we checked. - - _db.SaveChanges(); } - private void UserJoined(SocketGuildUser arg) + private Task UserJoined(SocketGuildUser arg) { var guild = UpsertChannel(arg.Guild); var defaultChannel = UpsertChannel(arg.Guild.DefaultChannel); defaultChannel.ParentChannel = guild; var u = UpsertAccount(arg, guild); u.DisplayName = arg.DisplayName; - } - private async Task ButtonHandler(SocketMessageComponent component) - { - switch (component.Data.CustomId) - { - case "custom-id": - await component.RespondAsync($"{component.User.Mention}, it's been here the whole time!"); - break; - } + return null; } internal static async Task SlashCommandHandler(SocketSlashCommand command) { @@ -187,35 +184,30 @@ public class DiscordInterface break; } } - internal vassago.Models.Attachment UpsertAttachment(IAttachment dAttachment) + internal static vassago.Models.Attachment UpsertAttachment(IAttachment dAttachment) { - var a = _db.Attachments.FirstOrDefault(ai => ai.ExternalId == dAttachment.Id); - if (a == null) - { - a = new vassago.Models.Attachment(); - _db.Attachments.Add(a); - } + var a = Rememberer.SearchAttachment(ai => ai.ExternalId == dAttachment.Id) + ?? new vassago.Models.Attachment(); + a.ContentType = dAttachment.ContentType; a.Description = dAttachment.Description; a.Filename = dAttachment.Filename; a.Size = dAttachment.Size; a.Source = new Uri(dAttachment.Url); - + Rememberer.RememberAttachment(a); return a; } internal Message UpsertMessage(IUserMessage dMessage) { - var m = _db.Messages.FirstOrDefault(mi => mi.ExternalId == dMessage.Id.ToString() && mi.Protocol == PROTOCOL); - if (m == null) + var m = Rememberer.SearchMessage(mi => mi.ExternalId == dMessage.Id.ToString() && mi.Protocol == PROTOCOL) + ?? new() + { + Protocol = PROTOCOL + }; + + if (dMessage.Attachments?.Count > 0) { - m = new Message(); - m.Protocol = PROTOCOL; - _db.Messages.Add(m); - } - m.Attachments = m.Attachments ?? new List(); - if (dMessage.Attachments?.Any() == true) - { - m.Attachments = new List(); + m.Attachments = []; foreach (var da in dMessage.Attachments) { m.Attachments.Add(UpsertAttachment(da)); @@ -226,7 +218,8 @@ public class DiscordInterface m.Timestamp = dMessage.EditedTimestamp ?? dMessage.CreatedAt; m.Channel = UpsertChannel(dMessage.Channel); m.Author = UpsertAccount(dMessage.Author, m.Channel); - if(dMessage.Channel is IGuildChannel) + Console.WriteLine($"received message; author: {m.Author.DisplayName}, {m.Author.Id}"); + if (dMessage.Channel is IGuildChannel) { m.Author.DisplayName = (dMessage.Author as IGuildUser).DisplayName;//discord forgot how display names work. } @@ -234,97 +227,164 @@ public class DiscordInterface && (dMessage.MentionedUserIds?.FirstOrDefault(muid => muid == client.CurrentUser.Id) > 0)); m.Reply = (t) => { return dMessage.ReplyAsync(t); }; - m.React = (e) => { return attemptReact(dMessage, e); }; + m.React = (e) => { return AttemptReact(dMessage, e); }; + Rememberer.RememberMessage(m); return m; } internal Channel UpsertChannel(IMessageChannel channel) { - Channel c = _db.Channels.FirstOrDefault(ci => ci.ExternalId == channel.Id.ToString() && ci.Protocol == PROTOCOL); + Channel c = Rememberer.SearchChannel(ci => ci.ExternalId == channel.Id.ToString() && ci.Protocol == PROTOCOL); if (c == null) { - c = new Channel(); - _db.Channels.Add(c); + Console.WriteLine($"couldn't find channel under protocol {PROTOCOL} with externalId {channel.Id.ToString()}"); + c = new Channel() + { + Users = [] + }; } - c.DisplayName = channel.Name; c.ExternalId = channel.Id.ToString(); c.ChannelType = (channel is IPrivateChannel) ? vassago.Models.Enumerations.ChannelType.DM : vassago.Models.Enumerations.ChannelType.Normal; - c.Messages = c.Messages ?? new List(); + c.Messages ??= []; c.Protocol = PROTOCOL; if (channel is IGuildChannel) { + Console.WriteLine($"{channel.Name} is a guild channel. So i'm going to upsert the guild, {(channel as IGuildChannel).Guild}"); c.ParentChannel = UpsertChannel((channel as IGuildChannel).Guild); - c.ParentChannel.SubChannels.Add(c); } else if (channel is IPrivateChannel) { c.ParentChannel = protocolAsChannel; + Console.WriteLine("i'm a private channel so I'm setting my parent channel to the protocol as channel"); } else { c.ParentChannel = protocolAsChannel; Console.Error.WriteLine($"trying to upsert channel {channel.Id}/{channel.Name}, but it's neither guildchannel nor private channel. shrug.jpg"); } - c.SubChannels = c.SubChannels ?? new List(); + + Console.WriteLine($"upsertion of channel {c.DisplayName}, it's type {c.ChannelType}"); + switch (c.ChannelType) + { + case vassago.Models.Enumerations.ChannelType.DM: + var asPriv =(channel as IPrivateChannel); + var sender = asPriv?.Recipients?.FirstOrDefault(u => u.Id != client.CurrentUser.Id); // why yes, there's a list of recipients, and it's the sender. + if(sender != null) + { + c.DisplayName = "DM: " + sender.Username; + } + else + { + //I sent it, so I don't know the recipient's name. + } + break; + default: + c.DisplayName = channel.Name; + break; + } + + Channel parentChannel = null; + if (channel is IGuildChannel) + { + parentChannel = Rememberer.SearchChannel(c => c.ExternalId == (channel as IGuildChannel).Guild.Id.ToString() && c.Protocol == PROTOCOL); + if (parentChannel is null) + { + Console.Error.WriteLine("why am I still null?"); + } + } + else if (channel is IPrivateChannel) + { + parentChannel = protocolAsChannel; + } + else + { + parentChannel = protocolAsChannel; + Console.Error.WriteLine($"trying to upsert channel {channel.Id}/{channel.Name}, but it's neither guildchannel nor private channel. shrug.jpg"); + } + parentChannel.SubChannels ??= []; + if(!parentChannel.SubChannels.Contains(c)) + { + parentChannel.SubChannels.Add(c); + } + c.SendMessage = (t) => { return channel.SendMessageAsync(t); }; c.SendFile = (f, t) => { return channel.SendFileAsync(f, t); }; - switch(c.ChannelType) + c = Rememberer.RememberChannel(c); + + var selfAccountInChannel = c.Users.FirstOrDefault(a => a.ExternalId == client.CurrentUser.Id.ToString()); + if(selfAccountInChannel == null) { - case vassago.Models.Enumerations.ChannelType.DM: - c.DisplayName = "DM: " + (channel as IPrivateChannel).Recipients?.FirstOrDefault(u => u.Id != client.CurrentUser.Id).Username; - break; + selfAccountInChannel = UpsertAccount(client.CurrentUser, c); } + return c; } internal Channel UpsertChannel(IGuild channel) { - Channel c = _db.Channels.FirstOrDefault(ci => ci.ExternalId == channel.Id.ToString() && ci.Protocol == PROTOCOL); + Channel c = Rememberer.SearchChannel(ci => ci.ExternalId == channel.Id.ToString() && ci.Protocol == PROTOCOL); if (c == null) { + Console.WriteLine($"couldn't find channel under protocol {PROTOCOL} with externalId {channel.Id.ToString()}"); c = new Channel(); - _db.Channels.Add(c); } c.DisplayName = channel.Name; c.ExternalId = channel.Id.ToString(); - c.ChannelType = vassago.Models.Enumerations.ChannelType.Normal; - c.Messages = c.Messages ?? new List(); + c.ChannelType = vassago.Models.Enumerations.ChannelType.OU; + c.Messages ??= []; c.Protocol = protocolAsChannel.Protocol; c.ParentChannel = protocolAsChannel; - c.SubChannels = c.SubChannels ?? new List(); + c.SubChannels ??= []; c.MaxAttachmentBytes = channel.MaxUploadLimit; c.SendMessage = (t) => { throw new InvalidOperationException($"channel {channel.Name} is guild; cannot accept text"); }; c.SendFile = (f, t) => { throw new InvalidOperationException($"channel {channel.Name} is guild; send file"); }; - return c; + + return Rememberer.RememberChannel(c); } - internal Account UpsertAccount(IUser user, Channel inChannel) + internal static Account UpsertAccount(IUser discordUser, Channel inChannel) { - var acc = _db.Accounts.FirstOrDefault(ui => ui.ExternalId == user.Id.ToString() && ui.SeenInChannel.Id == inChannel.Id); - if (acc == null) + var acc = Rememberer.SearchAccount(ui => ui.ExternalId == discordUser.Id.ToString() && ui.SeenInChannel.Id == inChannel.Id); + Console.WriteLine($"upserting account, retrieved {acc?.Id}."); + if (acc != null) { - acc = new Account(); - _db.Accounts.Add(acc); + Console.WriteLine($"acc's user: {acc.IsUser?.Id}"); } - acc.Username = user.Username; - acc.ExternalId = user.Id.ToString(); - acc.IsBot = user.IsBot || user.IsWebhook; + acc ??= new Account() { + IsUser = Rememberer.SearchUser(u => u.Accounts.Any(a => a.ExternalId == discordUser.Id.ToString() && a.Protocol == PROTOCOL)) + ?? new User() + }; + + acc.Username = discordUser.Username; + acc.ExternalId = discordUser.Id.ToString(); + acc.IsBot = discordUser.IsBot || discordUser.IsWebhook; acc.Protocol = PROTOCOL; acc.SeenInChannel = inChannel; - acc.IsUser = _db.Users.FirstOrDefault(u => u.Accounts.Any(a => a.ExternalId == acc.ExternalId && a.Protocol == acc.Protocol)); - if(acc.IsUser == null) + Console.WriteLine($"we asked rememberer to search for acc's user. {acc.IsUser?.Id}"); + if (acc.IsUser != null) { - acc.IsUser = new User() { Accounts = new List() { acc } }; - _db.Users.Add(acc.IsUser); + Console.WriteLine($"user has record of {acc.IsUser.Accounts?.Count ?? 0} accounts"); + } + acc.IsUser ??= new User() { Accounts = [acc] }; + if (inChannel.Users?.Count > 0) + { + Console.WriteLine($"channel has {inChannel.Users.Count} accounts"); + } + Rememberer.RememberAccount(acc); + inChannel.Users ??= []; + if(!inChannel.Users.Contains(acc)) + { + inChannel.Users.Add(acc); + Rememberer.RememberChannel(inChannel); } return acc; } - private Task attemptReact(IUserMessage msg, string e) + private static Task AttemptReact(IUserMessage msg, string e) { - var c = _db.Channels.FirstOrDefault(c => c.ExternalId == msg.Channel.Id.ToString()); + var c = Rememberer.SearchChannel(c => c.ExternalId == msg.Channel.Id.ToString());// db.Channels.FirstOrDefault(c => c.ExternalId == msg.Channel.Id.ToString()); //var preferredEmote = c.EmoteOverrides?[e] ?? e; //TODO: emote overrides var preferredEmote = e; if (Emoji.TryParse(preferredEmote, out Emoji emoji)) diff --git a/ProtocolInterfaces/DiscordInterface/SlashCommandsHelper.cs b/ProtocolInterfaces/DiscordInterface/SlashCommandsHelper.cs index 95ad279..a1cba8f 100644 --- a/ProtocolInterfaces/DiscordInterface/SlashCommandsHelper.cs +++ b/ProtocolInterfaces/DiscordInterface/SlashCommandsHelper.cs @@ -7,7 +7,7 @@ using Discord.WebSocket; using Discord; using Discord.Net; -namespace vassago.DiscordInterface +namespace vassago.ProtocolInterfaces.DiscordInterface { public static class SlashCommandsHelper { diff --git a/README.md b/README.md index ca9e08f..a2e53b5 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,16 @@ that's read messages/view channels, send messages, send messages in threads, and ## Data Types +database diagram. is a fancy term. + +message 1:n attachment +user 1:n account +channel 1:n account +channel 1:n message +account 1:n message + +featurepermission n:n ? + ### Accounts a `User` can have multiple `Account`s. e.g., @adam:greyn.club? that's an "account". I, however, am a `User`. An `Account` has references to the `Channels` its seen in - as in, leaf-level. If you're in a subchannel, you'll have an appropriate listing there - i.e., you will never have an account in "discord (itself)", you'll have one in the guild text-channels diff --git a/Rememberer.cs b/Rememberer.cs new file mode 100644 index 0000000..8d8d830 --- /dev/null +++ b/Rememberer.cs @@ -0,0 +1,110 @@ +namespace vassago; + +using System.Linq.Expressions; +using vassago.Models; +using Microsoft.EntityFrameworkCore; + +public static class Rememberer +{ + private static readonly ChattingContext db = new(); + public static Account SearchAccount(Expression> predicate) + { + return db.Accounts.Include(a => a.IsUser).FirstOrDefault(predicate); + } + public static List SearchAccounts(Expression> predicate) + { + return db.Accounts.Where(predicate).ToList(); + } + public static Attachment SearchAttachment(Expression> predicate) + { + return db.Attachments.FirstOrDefault(predicate); + } + public static Channel SearchChannel(Expression> predicate) + { + return db.Channels.FirstOrDefault(predicate); + } + public static Message SearchMessage(Expression> predicate) + { + return db.Messages.FirstOrDefault(predicate); + } + public static User SearchUser(Expression> predicate) + { + return db.Users.Include(u => u.Accounts).FirstOrDefault(predicate); + } + public static void RememberAccount(Account toRemember) + { + toRemember.IsUser ??= new User{ Accounts = [toRemember]}; + db.Update(toRemember.IsUser); + db.SaveChanges(); + } + public static void RememberAttachment(Attachment toRemember) + { + toRemember.Message ??= new Message() { Attachments = [toRemember]}; + db.Update(toRemember.Message); + db.SaveChanges(); + } + public static Channel RememberChannel(Channel toRemember) + { + db.Update(toRemember); + db.SaveChanges(); + return toRemember; + } + public static void RememberMessage(Message toRemember) + { + toRemember.Channel ??= new (){ Messages = [toRemember] }; + db.Update(toRemember.Channel); + db.SaveChanges(); + } + public static void RememberUser(User toRemember) + { + db.Users.Update(toRemember); + + db.SaveChanges(); + } + public static void ForgetAccount(Account toForget) + { + var user = toForget.IsUser; + var usersOnlyAccount = user.Accounts?.Count == 1; + + if(usersOnlyAccount) + { + Rememberer.ForgetUser(user); + } + else + { + db.Accounts.Remove(toForget); + db.SaveChanges(); + } + } + public static void ForgetChannel(Channel toForget) + { + db.Channels.Remove(toForget); + + db.SaveChanges(); + } + public static void ForgetUser(User toForget) + { + db.Users.Remove(toForget); + + db.SaveChanges(); + } + public static List AccountsOverview() + { + return [..db.Accounts]; + } + public static List ChannelsOverview() + { + return [..db.Channels.Include(u => u.SubChannels).Include(c => c.ParentChannel)]; + } + public static Channel ChannelDetail(Guid Id) + { + return db.Channels.Find(Id); + // .Include(u => u.SubChannels) + // .Include(u => u.Users) + // .Include(u => u.ParentChannel); + } + public static List UsersOverview() + { + return db.Users.ToList(); + } +} \ No newline at end of file diff --git a/WebInterface/Controllers/AccountsController.cs b/WebInterface/Controllers/AccountsController.cs new file mode 100644 index 0000000..09e26e6 --- /dev/null +++ b/WebInterface/Controllers/AccountsController.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using vassago.Models; +using vassago.WebInterface.Models; + +namespace vassago.WebInterface.Controllers; + +public class AccountsController(ChattingContext db) : Controller +{ + private ChattingContext Database => db; + + public async Task Index() + { + return Database.Accounts != null ? + View(await Database.Accounts.ToListAsync()) : + Problem("Entity set '_db.Accounts' is null."); + } + public async Task Details(Guid id) + { + var account = await Database.Accounts + .Include(a => a.IsUser) + .Include(a => a.SeenInChannel) + .FirstAsync(a => a.Id == id); + return Database.Accounts != null ? + View(account) : + Problem("Entity set '_db.Accounts' is null."); + } + + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() + { + return View(new ErrorPageViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } +} \ No newline at end of file diff --git a/WebInterface/Controllers/ChannelsController.cs b/WebInterface/Controllers/ChannelsController.cs index 93b4b75..a5c4c95 100644 --- a/WebInterface/Controllers/ChannelsController.cs +++ b/WebInterface/Controllers/ChannelsController.cs @@ -4,40 +4,23 @@ using System.Text; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using vassago.Models; +using vassago.WebInterface.Models; -namespace vassago.Controllers; +namespace vassago.WebInterface.Controllers; -public class ChannelsController : Controller +public class ChannelsController() : Controller { - private readonly ILogger _logger; - private readonly ChattingContext _db; - - public ChannelsController(ILogger logger, ChattingContext db) - { - _logger = logger; - _db = db; - } - - public async Task Index(string searchString) - { - return _db.Channels != null ? - View(_db.Channels.Include(u => u.ParentChannel).ToList().OrderBy(c => c.LineageSummary)) : - Problem("Entity set '_db.Channels' is null."); - } public async Task Details(Guid id) { - if(_db.Channels == null) + var allChannels = Rememberer.ChannelsOverview(); + if(allChannels == null) return Problem("Entity set '_db.Channels' is null."); //"but adam", says the strawman, "why load *every* channel and walk your way up? surely there's a .Load command that works or something." //eh. I checked. Not really. You could make an SQL view that recurses its way up, meh idk how. You could just eagerly load *every* related object... //but that would take in all the messages. //realistically I expect this will have less than 1MB of total "channels", and several GB of total messages per (text) channel. - var AllChannels = await _db.Channels - .Include(u => u.SubChannels) - .Include(u => u.Users) - .Include(u => u.ParentChannel) - .ToListAsync(); - var channel = AllChannels.First(u => u.Id == id); + + var channel = allChannels.First(u => u.Id == id); var walker = channel; while(walker != null) { @@ -46,7 +29,7 @@ public class ChannelsController : Controller walker = walker.ParentChannel; } var sb = new StringBuilder(); - sb.Append("["); + sb.Append('['); sb.Append($"{{text: \"{channel.SubChannels?.Count}\", nodes: ["); var first=true; foreach(var subChannel in channel.SubChannels) diff --git a/WebInterface/Controllers/HomeController.cs b/WebInterface/Controllers/HomeController.cs index b790795..f0d9b00 100644 --- a/WebInterface/Controllers/HomeController.cs +++ b/WebInterface/Controllers/HomeController.cs @@ -5,30 +5,30 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments; using vassago.Models; +using vassago.WebInterface.Models; namespace vassago.Controllers; public class HomeController : Controller { private readonly ILogger _logger; - private readonly ChattingContext _db; - public HomeController(ILogger logger, ChattingContext db) + public HomeController(ILogger logger) { _logger = logger; - _db = db; } public IActionResult Index() { - var allAccounts = _db.Accounts.ToList(); - var allChannels = _db.Channels.Include(c => c.Users).ToList(); + var allAccounts = Rememberer.AccountsOverview(); + var allChannels = Rememberer.ChannelsOverview(); + Console.WriteLine($"accounts: {allAccounts?.Count ?? 0}, channels: {allChannels?.Count ?? 0}"); var sb = new StringBuilder(); - sb.Append("["); - sb.Append("{text: \"channels\", nodes: ["); + sb.Append('['); + sb.Append("{text: \"channels\", expanded:true, nodes: ["); var first = true; - var topLevelChannels = _db.Channels.Where(x => x.ParentChannel == null); + var topLevelChannels = Rememberer.ChannelsOverview().Where(x => x.ParentChannel == null); foreach (var topLevelChannel in topLevelChannels) { if (first) @@ -46,7 +46,7 @@ public class HomeController : Controller if (allChannels.Any()) { - sb.Append(",{text: \"orphaned channels\", nodes: ["); + sb.Append(",{text: \"orphaned channels\", expanded:true, nodes: ["); first = true; while (true) { @@ -68,7 +68,7 @@ public class HomeController : Controller } if (allAccounts.Any()) { - sb.Append(",{text: \"channelless accounts\", nodes: ["); + sb.Append(",{text: \"channelless accounts\", expanded:true, nodes: ["); first = true; foreach (var acc in allAccounts) { @@ -84,13 +84,13 @@ public class HomeController : Controller } sb.Append("]}"); } - var users = _db.Users.ToList(); + var users = Rememberer.UsersOverview();// _db.Users.ToList(); if(users.Any()) { - sb.Append(",{text: \"users\", nodes: ["); + sb.Append(",{text: \"users\", expanded:true, nodes: ["); first=true; //refresh list; we'll be knocking them out again in serializeUser - allAccounts = _db.Accounts.ToList(); + allAccounts = Rememberer.AccountsOverview(); foreach(var user in users) { if (first) @@ -105,7 +105,7 @@ public class HomeController : Controller } sb.Append("]}"); } - sb.Append("]"); + sb.Append(']'); ViewData.Add("treeString", sb.ToString()); return View("Index"); } @@ -114,6 +114,7 @@ public class HomeController : Controller allChannels.Remove(currentChannel); //"but adam", you say, "there's an href attribute, why make a link?" because that makes the entire bar a link, and trying to expand the node will probably click the link sb.Append($"{{\"text\": \"{currentChannel.DisplayName}\""); + sb.Append(", expanded:true "); var theseAccounts = allAccounts.Where(a => a.SeenInChannel?.Id == currentChannel.Id).ToList(); allAccounts.RemoveAll(a => a.SeenInChannel?.Id == currentChannel.Id); var first = true; @@ -123,7 +124,7 @@ public class HomeController : Controller } if (currentChannel.SubChannels != null) { - foreach (var subChannel in currentChannel.SubChannels ?? new List()) + foreach (var subChannel in currentChannel.SubChannels) { if (first) { @@ -135,7 +136,7 @@ public class HomeController : Controller } serializeChannel(ref sb, ref allChannels, ref allAccounts, subChannel); } - if (theseAccounts != null) + if (theseAccounts != null && !first) //"first" here tells us that we have at least one subchannel { sb.Append(','); } @@ -162,11 +163,10 @@ public class HomeController : Controller } private void serializeAccount(ref StringBuilder sb, Account currentAccount) { - sb.Append($"{{\"text\": \"{currentAccount.DisplayName}\"}}"); + sb.Append($"{{\"text\": \"{currentAccount.DisplayName}\"}}"); } private void serializeUser(ref StringBuilder sb, ref List allAccounts, User currentUser) { - Console.WriteLine(currentUser); sb.Append($"{{\"text\": \""); sb.Append(currentUser.DisplayName); sb.Append("\", "); diff --git a/WebInterface/Controllers/UsersController.cs b/WebInterface/Controllers/UsersController.cs index ae37360..f18ba0a 100644 --- a/WebInterface/Controllers/UsersController.cs +++ b/WebInterface/Controllers/UsersController.cs @@ -2,37 +2,31 @@ using System.Diagnostics; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using vassago.Models; +using vassago.WebInterface.Models; -namespace vassago.Controllers; +namespace vassago.WebInterface.Controllers; -public class UsersController : Controller +public class UsersController(ChattingContext db) : Controller { - private readonly ILogger _logger; - private readonly ChattingContext _db; + private ChattingContext Database => db; - public UsersController(ILogger logger, ChattingContext db) + public async Task Index() { - _logger = logger; - _db = db; - } - - public async Task Index(string searchString) - { - return _db.Users != null ? - View(await _db.Users.Include(u => u.Accounts).ToListAsync()) : + return Database.Users != null ? + View(await Database.Users.Include(u => u.Accounts).ToListAsync()) : Problem("Entity set '_db.Users' is null."); } public async Task Details(Guid id) { - var user = await _db.Users + var user = await Database.Users .Include(u => u.Accounts) .FirstAsync(u => u.Id == id); - var allTheChannels = await _db.Channels.ToListAsync(); + var allTheChannels = await Database.Channels.ToListAsync(); foreach(var acc in user.Accounts) { acc.SeenInChannel = allTheChannels.FirstOrDefault(c => c.Id == acc.SeenInChannel.Id); } - return _db.Users != null ? + return Database.Users != null ? View(user) : Problem("Entity set '_db.Users' is null."); } diff --git a/WebInterface/Controllers/api/ChannelsControler.cs b/WebInterface/Controllers/api/ChannelsControler.cs index 8487e64..3001123 100644 --- a/WebInterface/Controllers/api/ChannelsControler.cs +++ b/WebInterface/Controllers/api/ChannelsControler.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using vassago.Models; +using vassago.ProtocolInterfaces.DiscordInterface; namespace vassago.Controllers.api; @@ -10,49 +11,50 @@ namespace vassago.Controllers.api; public class ChannelsController : ControllerBase { private readonly ILogger _logger; - private readonly ChattingContext _db; - public ChannelsController(ILogger logger, ChattingContext db) + public ChannelsController(ILogger logger) { _logger = logger; - _db = db; } [HttpGet("{id}")] [Produces("application/json")] public Channel Get(Guid id) { - return _db.Find(id); + return Rememberer.ChannelDetail(id); } [HttpPatch] [Produces("application/json")] public IActionResult Patch([FromBody] Channel channel) { - var fromDb = _db.Channels.Find(channel.Id); + var fromDb = Rememberer.ChannelDetail(channel.Id); if (fromDb == null) { _logger.LogError($"attempt to update channel {channel.Id}, not found"); return NotFound(); + } + else + { + _logger.LogDebug($"patching {channel.DisplayName} (id: {channel.Id})"); } //settable values: lewdness filter level, meanness filter level. maybe i could decorate them... fromDb.LewdnessFilterLevel = channel.LewdnessFilterLevel; fromDb.MeannessFilterLevel = channel.MeannessFilterLevel; - _db.SaveChanges(); + Rememberer.RememberChannel(fromDb); return Ok(fromDb); } [HttpDelete] [Produces("application/json")] public IActionResult Delete([FromBody] Channel channel) { - var fromDb = _db.Channels.Find(channel.Id); + var fromDb = Rememberer.ChannelDetail(channel.Id); if (fromDb == null) { _logger.LogError($"attempt to delete channel {channel.Id}, not found"); return NotFound(); } deleteChannel(fromDb); - _db.SaveChanges(); return Ok(); } private void deleteChannel(Channel channel) @@ -73,21 +75,16 @@ public class ChannelsController : ControllerBase } } - if(channel.Messages?.Count > 0) - { - _db.Remove(channel.Messages); - } - - _db.Remove(channel); + Rememberer.ForgetChannel(channel); } private void deleteAccount(Account account) { var user = account.IsUser; var usersOnlyAccount = user.Accounts?.Count == 1; - _db.Remove(account); + Rememberer.ForgetAccount(account); if(usersOnlyAccount) - _db.Users.Remove(user); + Rememberer.ForgetUser(user); } } diff --git a/WebInterface/Controllers/api/UsersController.cs b/WebInterface/Controllers/api/UsersController.cs new file mode 100644 index 0000000..c522461 --- /dev/null +++ b/WebInterface/Controllers/api/UsersController.cs @@ -0,0 +1,40 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using vassago.Models; +using vassago.ProtocolInterfaces.DiscordInterface; + +namespace vassago.Controllers.api; + +[Route("api/[controller]")] +[ApiController] +public class UsersController : ControllerBase +{ + private readonly ILogger _logger; + + public UsersController(ILogger logger) + { + _logger = logger; + } + + [HttpPatch] + [Produces("application/json")] + public IActionResult Patch([FromBody] User user) + { + var fromDb = Rememberer.SearchUser(u => u.Id == user.Id); + if (fromDb == null) + { + _logger.LogError($"attempt to update user {user.Id}, not found"); + return NotFound(); + } + else + { + _logger.LogDebug($"patching {user.DisplayName} (id: {user.Id})"); + } + + //TODO: settable values: display name + //fromDb.DisplayName = user.DisplayName; + Rememberer.RememberUser(fromDb); + return Ok(fromDb); + } +} \ No newline at end of file diff --git a/WebInterface/Controllers/ErrorPageViewModel.cs b/WebInterface/Models/ErrorPageViewModel.cs similarity index 54% rename from WebInterface/Controllers/ErrorPageViewModel.cs rename to WebInterface/Models/ErrorPageViewModel.cs index def8bca..a013dc1 100644 --- a/WebInterface/Controllers/ErrorPageViewModel.cs +++ b/WebInterface/Models/ErrorPageViewModel.cs @@ -1,8 +1,8 @@ -namespace vassago.Models; +namespace vassago.WebInterface.Models; public class ErrorPageViewModel { - public string? RequestId { get; set; } + public string RequestId { get; set; } public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); } diff --git a/WebInterface/Views/Accounts/Details.cshtml b/WebInterface/Views/Accounts/Details.cshtml new file mode 100644 index 0000000..4a18080 --- /dev/null +++ b/WebInterface/Views/Accounts/Details.cshtml @@ -0,0 +1,68 @@ +@model Account +@using Newtonsoft.Json +@using System.Text +@{ + ViewData["Title"] = "Account details"; +} + +home/@Html.Raw(ViewData["breadcrumbs"]) + + + + + + + + + + + + + + + +
belongs to user@Model.IsUser.DisplayName +
Seen in channel
Permission Tags +
+
+ +@section Scripts{ + + +} diff --git a/WebInterface/Views/Channels/Details.cshtml b/WebInterface/Views/Channels/Details.cshtml index 8d9d549..4a40142 100644 --- a/WebInterface/Views/Channels/Details.cshtml +++ b/WebInterface/Views/Channels/Details.cshtml @@ -1,5 +1,6 @@ @using System.ComponentModel @using Newtonsoft.Json +@using System.Text; @model Tuple @{ var ThisChannel = Model.Item1; @@ -91,7 +92,16 @@ Accounts - @(ThisChannel.Users?.Count ?? 0) + + @if((ThisChannel.Users?.Count ?? 0) > 0) + { + @Html.Raw("
"); + } + else + { + @Html.Raw("none") + } + @@ -105,9 +115,9 @@ } \ No newline at end of file diff --git a/WebInterface/Views/Home/Index.cshtml b/WebInterface/Views/Home/Index.cshtml index 446233a..4f5544e 100644 --- a/WebInterface/Views/Home/Index.cshtml +++ b/WebInterface/Views/Home/Index.cshtml @@ -1,8 +1,7 @@ @{ ViewData["Title"] = "Home Page"; } -
-tree above. +
tree here
@section Scripts{ } diff --git a/devuitls.sh b/devuitls.sh index 5c603de..d0f1748 100755 --- a/devuitls.sh +++ b/devuitls.sh @@ -26,7 +26,7 @@ case "$1" in "db-fullreset") sudo -u postgres psql -c "drop database ${servicename}_dev;" - sudo -u postgres psql -c "delete user $servicename" + sudo -u postgres psql -c "drop user $servicename" $0 "initial" ;; *) diff --git a/vassago.csproj b/vassago.csproj index dae7d44..3c3c5ba 100644 --- a/vassago.csproj +++ b/vassago.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable $(NoWarn);CA2254