Compare commits

...

14 Commits

Author SHA1 Message Date
4e82eedf9c Merge branch 'i-fucking-hate-database'
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
Conflicts:
	ProtocolInterfaces/DiscordInterface/DiscordInterface.cs
2025-03-18 21:29:55 -04:00
b6f74f580c pages all work 2025-03-18 20:14:52 -04:00
488a89614a account details view
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
lineage summary doesn't work
2025-03-17 23:38:16 -04:00
d22faae2f6 vassago is back to 0! sort of!
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-03-12 18:34:39 -04:00
6881816c94 self referencing serialization ignored
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-03-12 16:05:22 -04:00
53753374f0 runs and listens without exploding 2025-03-11 22:20:09 -04:00
50ecfc5867 double add solved. also, i was relinking... I think that's supposed to have been only if needed. 2025-03-11 13:33:00 -04:00
0d3a56c8db double-adding of Accounts is sovled - but now it's double-adding Users.
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
whyyyy
2025-03-06 17:02:33 -05:00
18e8f0f36e issue cracked. now, apply rememberer to webinterface. 2025-03-05 23:11:58 -05:00
d006367ecc you know why its change is being tracked? because you have another db context. 2025-02-28 22:53:10 -05:00
c971add137 a lot of my problems are who owns what
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-02-27 16:17:26 -05:00
3ed37959ad fixed most compiler complaints in discord interface
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-02-25 23:45:30 -05:00
736e3cb763 it's always something
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
2025-02-22 22:14:16 -05:00
740471d105 rememberer. but it doesn't remember.
Some checks failed
gitea.arg.rip/vassago/pipeline/head There was a failure building this commit
FUCK this is what I get for saying I like entity framework. *adds thing* *hits save* "(unrelated shit) is already here, why are you trying to add it again you dumbass?" FUCK IF I KNOW, you're supposed to be straightening this shit out!
2025-02-07 17:00:29 -05:00
25 changed files with 602 additions and 268 deletions

2
.vscode/launch.json vendored
View File

@ -10,7 +10,7 @@
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path. // 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": [], "args": [],
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"stopAtEntry": false, "stopAtEntry": false,

View File

@ -8,6 +8,7 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Collections.Generic; using System.Collections.Generic;
using vassago.ProtocolInterfaces.DiscordInterface;
public class Behaver public class Behaver
{ {
@ -72,55 +73,31 @@ public class Behaver
public void MarkSelf(Account selfAccount) public void MarkSelf(Account selfAccount)
{ {
var db = new ChattingContext();
if(SelfUser == null) if(SelfUser == null)
{ {
SelfUser = selfAccount.IsUser; SelfUser = selfAccount.IsUser;
} }
else if (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(); SelfAccounts = Rememberer.SearchAccounts(a => a.IsUser == SelfUser);
db.SaveChanges(); 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}"); if(primary.Accounts == null)
primary.Accounts.AddRange(secondary.Accounts); primary.Accounts = new List<Account>();
if(secondary.Accounts != null)
primary.Accounts.AddRange(secondary.Accounts);
foreach(var a in secondary.Accounts) foreach(var a in secondary.Accounts)
{ {
a.IsUser = primary; a.IsUser = primary;
} }
secondary.Accounts.Clear(); secondary.Accounts.Clear();
Console.WriteLine("accounts transferred"); Rememberer.ForgetUser(secondary);
try Rememberer.RememberUser(primary);
{
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");
return true; return true;
} }
} }

View File

@ -72,7 +72,7 @@ public class LinkClose : Behavior
return true; return true;
} }
if(Behaver.Instance.CollapseUsers(_primary.IsUser, secondary, new ChattingContext())) if(Behaver.Instance.CollapseUsers(_primary.IsUser, secondary))
{ {
await message.Channel.SendMessage("done :)"); await message.Channel.SendMessage("done :)");
} }

View File

@ -4,10 +4,11 @@ namespace vassago
using vassago; using vassago;
using vassago.Models; using vassago.Models;
using vassago.TwitchInterface; using vassago.TwitchInterface;
using vassago.ProtocolInterfaces.DiscordInterface;
using System.Runtime.CompilerServices;
internal class ConsoleService : IHostedService internal class ConsoleService : IHostedService
{ {
public ConsoleService(IConfiguration aspConfig) public ConsoleService(IConfiguration aspConfig)
{ {
Shared.DBConnectionString = aspConfig["DBConnectionString"]; Shared.DBConnectionString = aspConfig["DBConnectionString"];
@ -21,14 +22,15 @@ namespace vassago
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
var initTasks = new List<Task>();
var dbc = new ChattingContext(); var dbc = new ChattingContext();
await dbc.Database.MigrateAsync(cancellationToken); await dbc.Database.MigrateAsync(cancellationToken);
if (DiscordTokens?.Any() ?? false) if (DiscordTokens?.Any() ?? false)
foreach (var dt in DiscordTokens) foreach (var dt in DiscordTokens)
{ {
var d = new DiscordInterface.DiscordInterface(); var d = new DiscordInterface();
await d.Init(dt); initTasks.Add(d.Init(dt));
ProtocolInterfaces.ProtocolList.discords.Add(d); ProtocolInterfaces.ProtocolList.discords.Add(d);
} }
@ -36,10 +38,11 @@ namespace vassago
foreach (var tc in TwitchConfigs) foreach (var tc in TwitchConfigs)
{ {
var t = new TwitchInterface.TwitchInterface(); var t = new TwitchInterface.TwitchInterface();
await t.Init(tc); initTasks.Add(t.Init(tc));
ProtocolInterfaces.ProtocolList.twitchs.Add(t); ProtocolInterfaces.ProtocolList.twitchs.Add(t);
} }
Console.WriteLine("survived initting");
Task.WaitAll(initTasks, cancellationToken);
} }
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)

View File

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection; using System.Reflection;
using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
public class Account public class Account
@ -27,5 +28,6 @@ public class Account
public bool IsBot { get; set; } //webhook counts public bool IsBot { get; set; } //webhook counts
public Channel SeenInChannel { get; set; } public Channel SeenInChannel { get; set; }
public string Protocol { get; set; } public string Protocol { get; set; }
[JsonIgnore]
public User IsUser {get; set;} public User IsUser {get; set;}
} }

View File

@ -7,6 +7,7 @@ using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using static vassago.Models.Enumerations; using static vassago.Models.Enumerations;
public class Channel public class Channel
@ -17,6 +18,7 @@ public class Channel
public string DisplayName { get; set; } public string DisplayName { get; set; }
[DeleteBehavior(DeleteBehavior.Cascade)] [DeleteBehavior(DeleteBehavior.Cascade)]
public List<Channel> SubChannels { get; set; } public List<Channel> SubChannels { get; set; }
[JsonIgnore]
public Channel ParentChannel { get; set; } public Channel ParentChannel { get; set; }
public string Protocol { get; set; } public string Protocol { get; set; }
[DeleteBehavior(DeleteBehavior.Cascade)] [DeleteBehavior(DeleteBehavior.Cascade)]
@ -82,6 +84,23 @@ public class Channel
} }
} }
} }
///<summary>
///break self-referencing loops for library-agnostic serialization
///</summary>
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 public class DefinitePermissionSettings

View File

@ -11,7 +11,10 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<IHostedService, vassago.ConsoleService>(); builder.Services.AddSingleton<IHostedService, vassago.ConsoleService>();
builder.Services.AddDbContext<ChattingContext>(); builder.Services.AddDbContext<ChattingContext>();
builder.Services.AddControllers().AddNewtonsoftJson(); builder.Services.AddControllers().AddNewtonsoftJson(options => {
options.SerializerSettings.ReferenceLoopHandling =
Newtonsoft.Json.ReferenceLoopHandling.Ignore;
});
builder.Services.AddProblemDetails(); builder.Services.AddProblemDetails();
builder.Services.Configure<RazorViewEngineOptions>(o => { builder.Services.Configure<RazorViewEngineOptions>(o => {
o.ViewLocationFormats.Clear(); o.ViewLocationFormats.Clear();

View File

@ -13,22 +13,16 @@ using Microsoft.EntityFrameworkCore;
using System.Threading; using System.Threading;
using System.Reactive.Linq; using System.Reactive.Linq;
namespace vassago.DiscordInterface; namespace vassago.ProtocolInterfaces.DiscordInterface;
public class DiscordInterface public class DiscordInterface
{ {
internal const string PROTOCOL = "discord"; internal const string PROTOCOL = "discord";
internal DiscordSocketClient client; internal DiscordSocketClient client;
private bool eventsSignedUp = false; private bool eventsSignedUp = false;
private ChattingContext _db; private static readonly SemaphoreSlim discordChannelSetup = new(1, 1);
private static SemaphoreSlim discordChannelSetup = new SemaphoreSlim(1, 1);
private Channel protocolAsChannel; private Channel protocolAsChannel;
public DiscordInterface()
{
_db = new ChattingContext();
}
public async Task Init(string token) public async Task Init(string token)
{ {
await SetupDiscordChannel(); await SetupDiscordChannel();
@ -39,8 +33,8 @@ public class DiscordInterface
Console.WriteLine(msg.ToString()); Console.WriteLine(msg.ToString());
return Task.CompletedTask; return Task.CompletedTask;
}; };
client.Connected += SelfConnected; client.Connected += () => Task.Run(SelfConnected);
client.Ready += ClientReady; client.Ready += () => Task.Run(ClientReady);
await client.LoginAsync(TokenType.Bot, token); await client.LoginAsync(TokenType.Bot, token);
await client.StartAsync(); await client.StartAsync();
@ -52,7 +46,7 @@ public class DiscordInterface
try 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) if (protocolAsChannel == null)
{ {
protocolAsChannel = new Channel() protocolAsChannel = new Channel()
@ -66,13 +60,18 @@ public class DiscordInterface
ReactionsPossible = true, ReactionsPossible = true,
ExternalId = null, ExternalId = null,
Protocol = PROTOCOL, Protocol = PROTOCOL,
SubChannels = new List<Channel>() 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 finally
{ {
@ -89,7 +88,7 @@ public class DiscordInterface
client.MessageReceived += MessageReceived; client.MessageReceived += MessageReceived;
// _client.MessageUpdated += // _client.MessageUpdated +=
//client.UserJoined += UserJoined; client.UserJoined += UserJoined;
client.SlashCommandExecuted += SlashCommandHandler; client.SlashCommandExecuted += SlashCommandHandler;
//client.ChannelCreated += //client.ChannelCreated +=
// _client.ChannelDestroyed += // _client.ChannelDestroyed +=
@ -114,21 +113,29 @@ public class DiscordInterface
private async Task SelfConnected() private async Task SelfConnected()
{ {
var selfAccount = UpsertAccount(client.CurrentUser, protocolAsChannel); await discordChannelSetup.WaitAsync();
selfAccount.DisplayName = client.CurrentUser.Username;
await _db.SaveChangesAsync();
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) private async Task MessageReceived(SocketMessage messageParam)
{ {
var suMessage = messageParam as SocketUserMessage; if (messageParam is not SocketUserMessage)
if (suMessage == null)
{ {
Console.WriteLine($"{messageParam.Content}, but not a user message"); Console.WriteLine($"{messageParam.Content}, but not a user message");
return; 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}"); Console.WriteLine($"#{suMessage.Channel}[{DateTime.Now}][{suMessage.Author.Username} [id={suMessage.Author.Id}]][msg id: {suMessage.Id}] {suMessage.Content}");
var m = UpsertMessage(suMessage); var m = UpsertMessage(suMessage);
@ -140,26 +147,16 @@ public class DiscordInterface
} }
await Behaver.Instance.ActOn(m); 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. 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 guild = UpsertChannel(arg.Guild);
var defaultChannel = UpsertChannel(arg.Guild.DefaultChannel); var defaultChannel = UpsertChannel(arg.Guild.DefaultChannel);
defaultChannel.ParentChannel = guild; defaultChannel.ParentChannel = guild;
var u = UpsertAccount(arg, guild); var u = UpsertAccount(arg, guild);
u.DisplayName = arg.DisplayName; u.DisplayName = arg.DisplayName;
} return null;
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;
}
} }
internal static async Task SlashCommandHandler(SocketSlashCommand command) internal static async Task SlashCommandHandler(SocketSlashCommand command)
{ {
@ -187,35 +184,30 @@ public class DiscordInterface
break; 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); var a = Rememberer.SearchAttachment(ai => ai.ExternalId == dAttachment.Id)
if (a == null) ?? new vassago.Models.Attachment();
{
a = new vassago.Models.Attachment();
_db.Attachments.Add(a);
}
a.ContentType = dAttachment.ContentType; a.ContentType = dAttachment.ContentType;
a.Description = dAttachment.Description; a.Description = dAttachment.Description;
a.Filename = dAttachment.Filename; a.Filename = dAttachment.Filename;
a.Size = dAttachment.Size; a.Size = dAttachment.Size;
a.Source = new Uri(dAttachment.Url); a.Source = new Uri(dAttachment.Url);
Rememberer.RememberAttachment(a);
return a; return a;
} }
internal Message UpsertMessage(IUserMessage dMessage) internal Message UpsertMessage(IUserMessage dMessage)
{ {
var m = _db.Messages.FirstOrDefault(mi => mi.ExternalId == dMessage.Id.ToString() && mi.Protocol == PROTOCOL); var m = Rememberer.SearchMessage(mi => mi.ExternalId == dMessage.Id.ToString() && mi.Protocol == PROTOCOL)
if (m == null) ?? new()
{
Protocol = PROTOCOL
};
if (dMessage.Attachments?.Count > 0)
{ {
m = new Message(); m.Attachments = [];
m.Protocol = PROTOCOL;
_db.Messages.Add(m);
}
m.Attachments = m.Attachments ?? new List<vassago.Models.Attachment>();
if (dMessage.Attachments?.Any() == true)
{
m.Attachments = new List<vassago.Models.Attachment>();
foreach (var da in dMessage.Attachments) foreach (var da in dMessage.Attachments)
{ {
m.Attachments.Add(UpsertAttachment(da)); m.Attachments.Add(UpsertAttachment(da));
@ -226,7 +218,8 @@ public class DiscordInterface
m.Timestamp = dMessage.EditedTimestamp ?? dMessage.CreatedAt; m.Timestamp = dMessage.EditedTimestamp ?? dMessage.CreatedAt;
m.Channel = UpsertChannel(dMessage.Channel); m.Channel = UpsertChannel(dMessage.Channel);
m.Author = UpsertAccount(dMessage.Author, m.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. 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)); && (dMessage.MentionedUserIds?.FirstOrDefault(muid => muid == client.CurrentUser.Id) > 0));
m.Reply = (t) => { return dMessage.ReplyAsync(t); }; 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; return m;
} }
internal Channel UpsertChannel(IMessageChannel channel) 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) if (c == null)
{ {
c = new Channel(); Console.WriteLine($"couldn't find channel under protocol {PROTOCOL} with externalId {channel.Id.ToString()}");
_db.Channels.Add(c); c = new Channel()
{
Users = []
};
} }
c.DisplayName = channel.Name;
c.ExternalId = channel.Id.ToString(); c.ExternalId = channel.Id.ToString();
c.ChannelType = (channel is IPrivateChannel) ? vassago.Models.Enumerations.ChannelType.DM : vassago.Models.Enumerations.ChannelType.Normal; c.ChannelType = (channel is IPrivateChannel) ? vassago.Models.Enumerations.ChannelType.DM : vassago.Models.Enumerations.ChannelType.Normal;
c.Messages = c.Messages ?? new List<Message>(); c.Messages ??= [];
c.Protocol = PROTOCOL; c.Protocol = PROTOCOL;
if (channel is IGuildChannel) 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 = UpsertChannel((channel as IGuildChannel).Guild);
c.ParentChannel.SubChannels.Add(c);
} }
else if (channel is IPrivateChannel) else if (channel is IPrivateChannel)
{ {
c.ParentChannel = protocolAsChannel; c.ParentChannel = protocolAsChannel;
Console.WriteLine("i'm a private channel so I'm setting my parent channel to the protocol as channel");
} }
else else
{ {
c.ParentChannel = protocolAsChannel; c.ParentChannel = protocolAsChannel;
Console.Error.WriteLine($"trying to upsert channel {channel.Id}/{channel.Name}, but it's neither guildchannel nor private channel. shrug.jpg"); 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<Channel>();
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.SendMessage = (t) => { return channel.SendMessageAsync(t); };
c.SendFile = (f, t) => { return channel.SendFileAsync(f, 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: selfAccountInChannel = UpsertAccount(client.CurrentUser, c);
c.DisplayName = "DM: " + (channel as IPrivateChannel).Recipients?.FirstOrDefault(u => u.Id != client.CurrentUser.Id).Username;
break;
} }
return c; return c;
} }
internal Channel UpsertChannel(IGuild channel) 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) if (c == null)
{ {
Console.WriteLine($"couldn't find channel under protocol {PROTOCOL} with externalId {channel.Id.ToString()}");
c = new Channel(); c = new Channel();
_db.Channels.Add(c);
} }
c.DisplayName = channel.Name; c.DisplayName = channel.Name;
c.ExternalId = channel.Id.ToString(); c.ExternalId = channel.Id.ToString();
c.ChannelType = vassago.Models.Enumerations.ChannelType.Normal; c.ChannelType = vassago.Models.Enumerations.ChannelType.OU;
c.Messages = c.Messages ?? new List<Message>(); c.Messages ??= [];
c.Protocol = protocolAsChannel.Protocol; c.Protocol = protocolAsChannel.Protocol;
c.ParentChannel = protocolAsChannel; c.ParentChannel = protocolAsChannel;
c.SubChannels = c.SubChannels ?? new List<Channel>(); c.SubChannels ??= [];
c.MaxAttachmentBytes = channel.MaxUploadLimit; c.MaxAttachmentBytes = channel.MaxUploadLimit;
c.SendMessage = (t) => { throw new InvalidOperationException($"channel {channel.Name} is guild; cannot accept text"); }; 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"); }; 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); var acc = Rememberer.SearchAccount(ui => ui.ExternalId == discordUser.Id.ToString() && ui.SeenInChannel.Id == inChannel.Id);
if (acc == null) Console.WriteLine($"upserting account, retrieved {acc?.Id}.");
if (acc != null)
{ {
acc = new Account(); Console.WriteLine($"acc's user: {acc.IsUser?.Id}");
_db.Accounts.Add(acc);
} }
acc.Username = user.Username; acc ??= new Account() {
acc.ExternalId = user.Id.ToString(); IsUser = Rememberer.SearchUser(u => u.Accounts.Any(a => a.ExternalId == discordUser.Id.ToString() && a.Protocol == PROTOCOL))
acc.IsBot = user.IsBot || user.IsWebhook; ?? new User()
};
acc.Username = discordUser.Username;
acc.ExternalId = discordUser.Id.ToString();
acc.IsBot = discordUser.IsBot || discordUser.IsWebhook;
acc.Protocol = PROTOCOL; acc.Protocol = PROTOCOL;
acc.SeenInChannel = inChannel; acc.SeenInChannel = inChannel;
acc.IsUser = _db.Users.FirstOrDefault(u => u.Accounts.Any(a => a.ExternalId == acc.ExternalId && a.Protocol == acc.Protocol)); Console.WriteLine($"we asked rememberer to search for acc's user. {acc.IsUser?.Id}");
if(acc.IsUser == null) if (acc.IsUser != null)
{ {
acc.IsUser = new User() { Accounts = new List<Account>() { acc } }; Console.WriteLine($"user has record of {acc.IsUser.Accounts?.Count ?? 0} accounts");
_db.Users.Add(acc.IsUser); }
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; 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 = c.EmoteOverrides?[e] ?? e; //TODO: emote overrides
var preferredEmote = e; var preferredEmote = e;
if (Emoji.TryParse(preferredEmote, out Emoji emoji)) if (Emoji.TryParse(preferredEmote, out Emoji emoji))

View File

@ -7,7 +7,7 @@ using Discord.WebSocket;
using Discord; using Discord;
using Discord.Net; using Discord.Net;
namespace vassago.DiscordInterface namespace vassago.ProtocolInterfaces.DiscordInterface
{ {
public static class SlashCommandsHelper public static class SlashCommandsHelper
{ {

View File

@ -11,6 +11,16 @@ that's read messages/view channels, send messages, send messages in threads, and
## Data Types ## 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 ### 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 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

110
Rememberer.cs Normal file
View File

@ -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<Func<Account, bool>> predicate)
{
return db.Accounts.Include(a => a.IsUser).FirstOrDefault(predicate);
}
public static List<Account> SearchAccounts(Expression<Func<Account, bool>> predicate)
{
return db.Accounts.Where(predicate).ToList();
}
public static Attachment SearchAttachment(Expression<Func<Attachment, bool>> predicate)
{
return db.Attachments.FirstOrDefault(predicate);
}
public static Channel SearchChannel(Expression<Func<Channel, bool>> predicate)
{
return db.Channels.FirstOrDefault(predicate);
}
public static Message SearchMessage(Expression<Func<Message, bool>> predicate)
{
return db.Messages.FirstOrDefault(predicate);
}
public static User SearchUser(Expression<Func<User, bool>> 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<Account> AccountsOverview()
{
return [..db.Accounts];
}
public static List<Channel> 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<User> UsersOverview()
{
return db.Users.ToList();
}
}

View File

@ -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<IActionResult> Index()
{
return Database.Accounts != null ?
View(await Database.Accounts.ToListAsync()) :
Problem("Entity set '_db.Accounts' is null.");
}
public async Task<IActionResult> 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 });
}
}

View File

@ -4,40 +4,23 @@ using System.Text;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using vassago.Models; 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<ChannelsController> _logger;
private readonly ChattingContext _db;
public ChannelsController(ILogger<ChannelsController> logger, ChattingContext db)
{
_logger = logger;
_db = db;
}
public async Task<IActionResult> 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<IActionResult> Details(Guid id) public async Task<IActionResult> Details(Guid id)
{ {
if(_db.Channels == null) var allChannels = Rememberer.ChannelsOverview();
if(allChannels == null)
return Problem("Entity set '_db.Channels' is 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." //"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... //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. //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. //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) var channel = allChannels.First(u => u.Id == id);
.Include(u => u.Users)
.Include(u => u.ParentChannel)
.ToListAsync();
var channel = AllChannels.First(u => u.Id == id);
var walker = channel; var walker = channel;
while(walker != null) while(walker != null)
{ {
@ -46,7 +29,7 @@ public class ChannelsController : Controller
walker = walker.ParentChannel; walker = walker.ParentChannel;
} }
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.Append("["); sb.Append('[');
sb.Append($"{{text: \"{channel.SubChannels?.Count}\", nodes: ["); sb.Append($"{{text: \"{channel.SubChannels?.Count}\", nodes: [");
var first=true; var first=true;
foreach(var subChannel in channel.SubChannels) foreach(var subChannel in channel.SubChannels)

View File

@ -5,30 +5,30 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments; using Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments;
using vassago.Models; using vassago.Models;
using vassago.WebInterface.Models;
namespace vassago.Controllers; namespace vassago.Controllers;
public class HomeController : Controller public class HomeController : Controller
{ {
private readonly ILogger<HomeController> _logger; private readonly ILogger<HomeController> _logger;
private readonly ChattingContext _db;
public HomeController(ILogger<HomeController> logger, ChattingContext db) public HomeController(ILogger<HomeController> logger)
{ {
_logger = logger; _logger = logger;
_db = db;
} }
public IActionResult Index() public IActionResult Index()
{ {
var allAccounts = _db.Accounts.ToList(); var allAccounts = Rememberer.AccountsOverview();
var allChannels = _db.Channels.Include(c => c.Users).ToList(); var allChannels = Rememberer.ChannelsOverview();
Console.WriteLine($"accounts: {allAccounts?.Count ?? 0}, channels: {allChannels?.Count ?? 0}");
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.Append("["); sb.Append('[');
sb.Append("{text: \"channels\", nodes: ["); sb.Append("{text: \"channels\", expanded:true, nodes: [");
var first = true; 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) foreach (var topLevelChannel in topLevelChannels)
{ {
if (first) if (first)
@ -46,7 +46,7 @@ public class HomeController : Controller
if (allChannels.Any()) if (allChannels.Any())
{ {
sb.Append(",{text: \"orphaned channels\", nodes: ["); sb.Append(",{text: \"orphaned channels\", expanded:true, nodes: [");
first = true; first = true;
while (true) while (true)
{ {
@ -68,7 +68,7 @@ public class HomeController : Controller
} }
if (allAccounts.Any()) if (allAccounts.Any())
{ {
sb.Append(",{text: \"channelless accounts\", nodes: ["); sb.Append(",{text: \"channelless accounts\", expanded:true, nodes: [");
first = true; first = true;
foreach (var acc in allAccounts) foreach (var acc in allAccounts)
{ {
@ -84,13 +84,13 @@ public class HomeController : Controller
} }
sb.Append("]}"); sb.Append("]}");
} }
var users = _db.Users.ToList(); var users = Rememberer.UsersOverview();// _db.Users.ToList();
if(users.Any()) if(users.Any())
{ {
sb.Append(",{text: \"users\", nodes: ["); sb.Append(",{text: \"users\", expanded:true, nodes: [");
first=true; first=true;
//refresh list; we'll be knocking them out again in serializeUser //refresh list; we'll be knocking them out again in serializeUser
allAccounts = _db.Accounts.ToList(); allAccounts = Rememberer.AccountsOverview();
foreach(var user in users) foreach(var user in users)
{ {
if (first) if (first)
@ -105,7 +105,7 @@ public class HomeController : Controller
} }
sb.Append("]}"); sb.Append("]}");
} }
sb.Append("]"); sb.Append(']');
ViewData.Add("treeString", sb.ToString()); ViewData.Add("treeString", sb.ToString());
return View("Index"); return View("Index");
} }
@ -114,6 +114,7 @@ public class HomeController : Controller
allChannels.Remove(currentChannel); 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 //"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\": \"<a href=\\\"{Url.ActionLink(action: "Details", controller: "Channels", values: new {id = currentChannel.Id})}\\\">{currentChannel.DisplayName}</a>\""); sb.Append($"{{\"text\": \"<a href=\\\"{Url.ActionLink(action: "Details", controller: "Channels", values: new {id = currentChannel.Id})}\\\">{currentChannel.DisplayName}</a>\"");
sb.Append(", expanded:true ");
var theseAccounts = allAccounts.Where(a => a.SeenInChannel?.Id == currentChannel.Id).ToList(); var theseAccounts = allAccounts.Where(a => a.SeenInChannel?.Id == currentChannel.Id).ToList();
allAccounts.RemoveAll(a => a.SeenInChannel?.Id == currentChannel.Id); allAccounts.RemoveAll(a => a.SeenInChannel?.Id == currentChannel.Id);
var first = true; var first = true;
@ -123,7 +124,7 @@ public class HomeController : Controller
} }
if (currentChannel.SubChannels != null) if (currentChannel.SubChannels != null)
{ {
foreach (var subChannel in currentChannel.SubChannels ?? new List<Channel>()) foreach (var subChannel in currentChannel.SubChannels)
{ {
if (first) if (first)
{ {
@ -135,7 +136,7 @@ public class HomeController : Controller
} }
serializeChannel(ref sb, ref allChannels, ref allAccounts, subChannel); 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(','); sb.Append(',');
} }
@ -162,11 +163,10 @@ public class HomeController : Controller
} }
private void serializeAccount(ref StringBuilder sb, Account currentAccount) private void serializeAccount(ref StringBuilder sb, Account currentAccount)
{ {
sb.Append($"{{\"text\": \"{currentAccount.DisplayName}\"}}"); sb.Append($"{{\"text\": \"<a href=\\\"{Url.ActionLink(action: "Details", controller: "Accounts", values: new {id = currentAccount.Id})}\\\">{currentAccount.DisplayName}</a>\"}}");
} }
private void serializeUser(ref StringBuilder sb, ref List<Account> allAccounts, User currentUser) private void serializeUser(ref StringBuilder sb, ref List<Account> allAccounts, User currentUser)
{ {
Console.WriteLine(currentUser);
sb.Append($"{{\"text\": \"<a href=\\\"{Url.ActionLink(action: "Details", controller: "Users", values: new {id = currentUser.Id})}\\\">"); sb.Append($"{{\"text\": \"<a href=\\\"{Url.ActionLink(action: "Details", controller: "Users", values: new {id = currentUser.Id})}\\\">");
sb.Append(currentUser.DisplayName); sb.Append(currentUser.DisplayName);
sb.Append("</a>\", "); sb.Append("</a>\", ");

View File

@ -2,37 +2,31 @@ using System.Diagnostics;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using vassago.Models; 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<UsersController> _logger; private ChattingContext Database => db;
private readonly ChattingContext _db;
public UsersController(ILogger<UsersController> logger, ChattingContext db) public async Task<IActionResult> Index()
{ {
_logger = logger; return Database.Users != null ?
_db = db; View(await Database.Users.Include(u => u.Accounts).ToListAsync()) :
}
public async Task<IActionResult> Index(string searchString)
{
return _db.Users != null ?
View(await _db.Users.Include(u => u.Accounts).ToListAsync()) :
Problem("Entity set '_db.Users' is null."); Problem("Entity set '_db.Users' is null.");
} }
public async Task<IActionResult> Details(Guid id) public async Task<IActionResult> Details(Guid id)
{ {
var user = await _db.Users var user = await Database.Users
.Include(u => u.Accounts) .Include(u => u.Accounts)
.FirstAsync(u => u.Id == id); .FirstAsync(u => u.Id == id);
var allTheChannels = await _db.Channels.ToListAsync(); var allTheChannels = await Database.Channels.ToListAsync();
foreach(var acc in user.Accounts) foreach(var acc in user.Accounts)
{ {
acc.SeenInChannel = allTheChannels.FirstOrDefault(c => c.Id == acc.SeenInChannel.Id); acc.SeenInChannel = allTheChannels.FirstOrDefault(c => c.Id == acc.SeenInChannel.Id);
} }
return _db.Users != null ? return Database.Users != null ?
View(user) : View(user) :
Problem("Entity set '_db.Users' is null."); Problem("Entity set '_db.Users' is null.");
} }

View File

@ -2,6 +2,7 @@ using System.Diagnostics;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using vassago.Models; using vassago.Models;
using vassago.ProtocolInterfaces.DiscordInterface;
namespace vassago.Controllers.api; namespace vassago.Controllers.api;
@ -10,49 +11,50 @@ namespace vassago.Controllers.api;
public class ChannelsController : ControllerBase public class ChannelsController : ControllerBase
{ {
private readonly ILogger<ChannelsController> _logger; private readonly ILogger<ChannelsController> _logger;
private readonly ChattingContext _db;
public ChannelsController(ILogger<ChannelsController> logger, ChattingContext db) public ChannelsController(ILogger<ChannelsController> logger)
{ {
_logger = logger; _logger = logger;
_db = db;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
[Produces("application/json")] [Produces("application/json")]
public Channel Get(Guid id) public Channel Get(Guid id)
{ {
return _db.Find<Channel>(id); return Rememberer.ChannelDetail(id);
} }
[HttpPatch] [HttpPatch]
[Produces("application/json")] [Produces("application/json")]
public IActionResult Patch([FromBody] Channel channel) public IActionResult Patch([FromBody] Channel channel)
{ {
var fromDb = _db.Channels.Find(channel.Id); var fromDb = Rememberer.ChannelDetail(channel.Id);
if (fromDb == null) if (fromDb == null)
{ {
_logger.LogError($"attempt to update channel {channel.Id}, not found"); _logger.LogError($"attempt to update channel {channel.Id}, not found");
return NotFound(); return NotFound();
}
else
{
_logger.LogDebug($"patching {channel.DisplayName} (id: {channel.Id})");
} }
//settable values: lewdness filter level, meanness filter level. maybe i could decorate them... //settable values: lewdness filter level, meanness filter level. maybe i could decorate them...
fromDb.LewdnessFilterLevel = channel.LewdnessFilterLevel; fromDb.LewdnessFilterLevel = channel.LewdnessFilterLevel;
fromDb.MeannessFilterLevel = channel.MeannessFilterLevel; fromDb.MeannessFilterLevel = channel.MeannessFilterLevel;
_db.SaveChanges(); Rememberer.RememberChannel(fromDb);
return Ok(fromDb); return Ok(fromDb);
} }
[HttpDelete] [HttpDelete]
[Produces("application/json")] [Produces("application/json")]
public IActionResult Delete([FromBody] Channel channel) public IActionResult Delete([FromBody] Channel channel)
{ {
var fromDb = _db.Channels.Find(channel.Id); var fromDb = Rememberer.ChannelDetail(channel.Id);
if (fromDb == null) if (fromDb == null)
{ {
_logger.LogError($"attempt to delete channel {channel.Id}, not found"); _logger.LogError($"attempt to delete channel {channel.Id}, not found");
return NotFound(); return NotFound();
} }
deleteChannel(fromDb); deleteChannel(fromDb);
_db.SaveChanges();
return Ok(); return Ok();
} }
private void deleteChannel(Channel channel) private void deleteChannel(Channel channel)
@ -73,21 +75,16 @@ public class ChannelsController : ControllerBase
} }
} }
if(channel.Messages?.Count > 0) Rememberer.ForgetChannel(channel);
{
_db.Remove(channel.Messages);
}
_db.Remove(channel);
} }
private void deleteAccount(Account account) private void deleteAccount(Account account)
{ {
var user = account.IsUser; var user = account.IsUser;
var usersOnlyAccount = user.Accounts?.Count == 1; var usersOnlyAccount = user.Accounts?.Count == 1;
_db.Remove(account); Rememberer.ForgetAccount(account);
if(usersOnlyAccount) if(usersOnlyAccount)
_db.Users.Remove(user); Rememberer.ForgetUser(user);
} }
} }

View File

@ -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<ChannelsController> _logger;
public UsersController(ILogger<ChannelsController> 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);
}
}

View File

@ -1,8 +1,8 @@
namespace vassago.Models; namespace vassago.WebInterface.Models;
public class ErrorPageViewModel public class ErrorPageViewModel
{ {
public string? RequestId { get; set; } public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
} }

View File

@ -0,0 +1,68 @@
@model Account
@using Newtonsoft.Json
@using System.Text
@{
ViewData["Title"] = "Account details";
}
<a href="/">home</a>/@Html.Raw(ViewData["breadcrumbs"])
<table class="table">
<tbody>
<tr>
<th scope="row">belongs to user</th>
<td>@Model.IsUser.DisplayName</td>
<td><button alt="to do" disabled>separate</button></2td>
</tr>
<tr>
<th scope="row">Seen in channel</th>
<td class="account @Model.SeenInChannel.Protocol"><div class="protocol-icon">&nbsp;</div>@Model.SeenInChannel.LineageSummary<a href="/Channels/Details/@Model.SeenInChannel.Id">@Model.SeenInChannel.DisplayName</a></td>
</tr>
<tr>
<th scope="row">Permission Tags</th>
<td>
<div id="tagsTree"></div>
</td>
</tr>
</tbody>
</table>
@section Scripts{
<script type="text/javascript">
@{
var accountAsString = JsonConvert.SerializeObject(Model, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
}
const userOnLoad = @Html.Raw(accountAsString);
function jsonifyUser() {
var userNow = structuredClone(userOnLoad);
userNow.Accounts = null;
userNow.DisplayName = document.querySelector("#displayName").value;
console.log(userNow);
return userNow;
}
function getTagsTree() {
@{
var sb = new StringBuilder();
sb.Append("[{text: \"permission tags\", \"expanded\":true, nodes: [");
var first = true;
for (int i = 0; i < 1; i++)
{
if (!first)
sb.Append(',');
sb.Append($"{{text: \"<input type=\\\"checkbox\\\" > is goated (w/ sauce)</input>\"}}");
first = false;
}
sb.Append("]}]");
}
console.log(@Html.Raw(sb.ToString()));
var tree = @Html.Raw(sb.ToString());
return tree;
}
$('#tagsTree').bstreeview({ data: getTagsTree() });
document.querySelectorAll("input[type=checkbox]").forEach(node => { node.onchange = () => { patchModel(jsonifyUser(), '/api/Users/') } });
</script>
}

View File

@ -1,5 +1,6 @@
@using System.ComponentModel @using System.ComponentModel
@using Newtonsoft.Json @using Newtonsoft.Json
@using System.Text;
@model Tuple<Channel, Enumerations.LewdnessFilterLevel, Enumerations.MeannessFilterLevel> @model Tuple<Channel, Enumerations.LewdnessFilterLevel, Enumerations.MeannessFilterLevel>
@{ @{
var ThisChannel = Model.Item1; var ThisChannel = Model.Item1;
@ -91,7 +92,16 @@
</tr> </tr>
<tr> <tr>
<th scope="row">Accounts</th> <th scope="row">Accounts</th>
<td>@(ThisChannel.Users?.Count ?? 0)</td> <td>
@if((ThisChannel.Users?.Count ?? 0) > 0)
{
@Html.Raw("<div id=\"accountsTree\"></div>");
}
else
{
@Html.Raw("none")
}
</td>
</tr> </tr>
<tr> <tr>
<td colspan="2"> <td colspan="2">
@ -105,9 +115,9 @@
<script type="text/javascript"> <script type="text/javascript">
@{ @{
var modelAsString = JsonConvert.SerializeObject(ThisChannel, new JsonSerializerSettings var modelAsString = JsonConvert.SerializeObject(ThisChannel, new JsonSerializerSettings
{ {
ReferenceLoopHandling = ReferenceLoopHandling.Ignore ReferenceLoopHandling = ReferenceLoopHandling.Ignore
}); });
} }
const channelOnLoad = @Html.Raw(modelAsString); const channelOnLoad = @Html.Raw(modelAsString);
function jsonifyChannel() { function jsonifyChannel() {
@ -133,7 +143,27 @@
var tree = @Html.Raw(ViewData["channelsTree"]); var tree = @Html.Raw(ViewData["channelsTree"]);
return tree; return tree;
} }
function accountsTree() {
@{
var sb = new StringBuilder();
sb.Append("[{text: \"accounts\", \"expanded\":true, nodes: [");
var first = true;
foreach (var acc in ThisChannel.Users.OrderBy(a => a.SeenInChannel.LineageSummary))
{
if(!first)
sb.Append(',');
sb.Append($"{{text: \"<div class=\\\"account {acc.Protocol}\\\"><div class=\\\"protocol-icon\\\">&nbsp;</div>{acc.SeenInChannel.LineageSummary}/<a href=\\\"/Accounts/Details/{acc.Id}\\\">{acc.DisplayName}</a>\"}}");
first=false;
}
sb.Append("]}]");
}
//console.log(@Html.Raw(sb.ToString()));
var tree = @Html.Raw(sb.ToString());
return tree;
}
$('#channelsTree').bstreeview({ data: channelsTree() }); $('#channelsTree').bstreeview({ data: channelsTree() });
$('#accountsTree').bstreeview({ data: accountsTree() });
</script> </script>
} }

View File

@ -1,8 +1,7 @@
@{ @{
ViewData["Title"] = "Home Page"; ViewData["Title"] = "Home Page";
} }
<div id="tree"></div> <div id="tree">tree here</div>
tree above.
@section Scripts{ @section Scripts{
<script type="text/javascript"> <script type="text/javascript">

View File

@ -1,4 +1,4 @@
@model ErrorPageViewModel @model vassago.WebInterface.Models.ErrorPageViewModel
@{ @{
ViewData["Title"] = "Error"; ViewData["Title"] = "Error";
} }

View File

@ -4,36 +4,40 @@
@{ @{
ViewData["Title"] = "User details"; ViewData["Title"] = "User details";
} }
<table class="table">
<tbody>
<tr>
<td><input type="text" id="displayName" value="@Model.DisplayName"></input> <button onclick="patchModel(jsonifyUser(), @Html.Raw("'/api/Users/'"))">update</button></td>
</tr>
<tr>
<td>
<div id="accountsTree"></div>
</td>
</tr>
<tr>
<td>
<div id="tagsTree"></div>
</td>
</tr>
</tbody>
</table>
<a asp-controller="Accounts" asp-action="Details" asp-route-id="1">placeholderlink</a>
<a href="/">home</a>/@Html.Raw(ViewData["breadcrumbs"])
<table class="table">
<tbody>
<tr>
<th scope="row">Display Name (here)</th>
<td><input type="text" id="displayName" value="@Model.DisplayName" disabled alt="todo"></input> <button
onclick="patchModel(jsonifyUser(), @Html.Raw("'/api/Users/'"))" disabled alt"todo">update</button></td>
</tr>
<tr>
<th scope="row">Accounts</th>
<td>
<div id="accountsTree"></div>
</td>
</tr>
<tr>
<th scope="row">Permission Tags</th>
<td>
<div id="tagsTree"></div>
</td>
</tr>
</tbody>
</table>
@section Scripts{ @section Scripts{
<script type="text/javascript"> <script type="text/javascript">
@{ @{
var userAsString = JsonConvert.SerializeObject(Model, new JsonSerializerSettings var userAsString = JsonConvert.SerializeObject(Model, new JsonSerializerSettings
{ {
ReferenceLoopHandling = ReferenceLoopHandling.Ignore ReferenceLoopHandling = ReferenceLoopHandling.Ignore
}); });
} }
const userOnLoad = @Html.Raw(userAsString); const userOnLoad = @Html.Raw(userAsString);
function jsonifyUser() { function jsonifyUser() {
var userNow = structuredClone(userOnLoad); var userNow = structuredClone(userOnLoad);
userNow.Accounts = null; userNow.Accounts = null;
@ -43,44 +47,44 @@
return userNow; return userNow;
} }
function getAccountsTree() { function getAccountsTree() {
@{ @{
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.Append("[{text: \"accounts\", \"expanded\":true, nodes: ["); sb.Append("[{text: \"accounts\", \"expanded\":true, nodes: [");
var first = true; var first = true;
foreach (var acc in Model.Accounts) foreach (var acc in Model.Accounts.OrderBy(a => a.SeenInChannel.LineageSummary))
{ {
if(!first) if (!first)
sb.Append(','); sb.Append(',');
sb.Append($"{{text: \"<div class=\\\"account {acc.Protocol}\\\"><div class=\\\"protocol-icon\\\">&nbsp;</div>{acc.SeenInChannel.LineageSummary}/<a href=\\\"/Accounts/Details/{acc.Id}\\\">{acc.DisplayName}</a>\"}}"); sb.Append($"{{text: \"<div class=\\\"account {acc.Protocol}\\\"><div class=\\\"protocol-icon\\\">&nbsp;</div>{acc.SeenInChannel.LineageSummary}/<a href=\\\"/Accounts/Details/{acc.Id}\\\">{acc.DisplayName}</a>\"}}");
first=false; first = false;
}
sb.Append("]}]");
} }
console.log(@Html.Raw(sb.ToString())); sb.Append("]}]");
}
console.log(@Html.Raw(sb.ToString()));
var tree = @Html.Raw(sb.ToString()); var tree = @Html.Raw(sb.ToString());
return tree; return tree;
} }
function getTagsTree() { function getTagsTree() {
@{ @{
sb = new StringBuilder(); sb = new StringBuilder();
sb.Append("[{text: \"permission tags\", \"expanded\":true, nodes: ["); sb.Append("[{text: \"permission tags\", \"expanded\":true, nodes: [");
first = true; first = true;
for(int i = 0; i < 1; i++) for (int i = 0; i < 1; i++)
{ {
if(!first) if (!first)
sb.Append(','); sb.Append(',');
sb.Append($"{{text: \"<input type=\\\"checkbox\\\" > is goated (w/ sauce)</input>\"}}"); sb.Append($"{{text: \"<input type=\\\"checkbox\\\" > is goated (w/ sauce)</input>\"}}");
first=false; first = false;
}
sb.Append("]}]");
} }
console.log(@Html.Raw(sb.ToString())); sb.Append("]}]");
}
console.log(@Html.Raw(sb.ToString()));
var tree = @Html.Raw(sb.ToString()); var tree = @Html.Raw(sb.ToString());
return tree; return tree;
} }
$('#accountsTree').bstreeview({ data: getAccountsTree() }); $('#accountsTree').bstreeview({ data: getAccountsTree() });
$('#tagsTree').bstreeview({ data: getTagsTree() }); $('#tagsTree').bstreeview({ data: getTagsTree() });
document.querySelectorAll("input[type=checkbox]").forEach(node => {node.onchange = () => {patchModel(jsonifyUser(), '/api/Users/')}}); document.querySelectorAll("input[type=checkbox]").forEach(node => { node.onchange = () => { patchModel(jsonifyUser(), '/api/Users/') } });
</script> </script>
} }

View File

@ -26,7 +26,7 @@ case "$1" in
"db-fullreset") "db-fullreset")
sudo -u postgres psql -c "drop database ${servicename}_dev;" 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" $0 "initial"
;; ;;
*) *)

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);CA2254</NoWarn> <NoWarn>$(NoWarn);CA2254</NoWarn>
</PropertyGroup> </PropertyGroup>