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",
"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,

View File

@ -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<Account>();
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;
}
}

View File

@ -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 :)");
}

View File

@ -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<Task>();
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)

View File

@ -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;}
}

View File

@ -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<Channel> SubChannels { get; set; }
[JsonIgnore]
public Channel ParentChannel { get; set; }
public string Protocol { get; set; }
[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

View File

@ -11,7 +11,10 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<IHostedService, vassago.ConsoleService>();
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.Configure<RazorViewEngineOptions>(o => {
o.ViewLocationFormats.Clear();

View File

@ -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<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
{
@ -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<vassago.Models.Attachment>();
if (dMessage.Attachments?.Any() == true)
{
m.Attachments = new List<vassago.Models.Attachment>();
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<Message>();
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<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.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<Message>();
c.ChannelType = vassago.Models.Enumerations.ChannelType.OU;
c.Messages ??= [];
c.Protocol = protocolAsChannel.Protocol;
c.ParentChannel = protocolAsChannel;
c.SubChannels = c.SubChannels ?? new List<Channel>();
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<Account>() { 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))

View File

@ -7,7 +7,7 @@ using Discord.WebSocket;
using Discord;
using Discord.Net;
namespace vassago.DiscordInterface
namespace vassago.ProtocolInterfaces.DiscordInterface
{
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
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

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.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<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)
{
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)

View File

@ -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<HomeController> _logger;
private readonly ChattingContext _db;
public HomeController(ILogger<HomeController> logger, ChattingContext db)
public HomeController(ILogger<HomeController> 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\": \"<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();
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<Channel>())
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\": \"<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)
{
Console.WriteLine(currentUser);
sb.Append($"{{\"text\": \"<a href=\\\"{Url.ActionLink(action: "Details", controller: "Users", values: new {id = currentUser.Id})}\\\">");
sb.Append(currentUser.DisplayName);
sb.Append("</a>\", ");

View File

@ -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<UsersController> _logger;
private readonly ChattingContext _db;
private ChattingContext Database => db;
public UsersController(ILogger<UsersController> logger, ChattingContext db)
public async Task<IActionResult> Index()
{
_logger = logger;
_db = db;
}
public async Task<IActionResult> 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<IActionResult> 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.");
}

View File

@ -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<ChannelsController> _logger;
private readonly ChattingContext _db;
public ChannelsController(ILogger<ChannelsController> logger, ChattingContext db)
public ChannelsController(ILogger<ChannelsController> logger)
{
_logger = logger;
_db = db;
}
[HttpGet("{id}")]
[Produces("application/json")]
public Channel Get(Guid id)
{
return _db.Find<Channel>(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);
}
}

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 string? RequestId { get; set; }
public string RequestId { get; set; }
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 Newtonsoft.Json
@using System.Text;
@model Tuple<Channel, Enumerations.LewdnessFilterLevel, Enumerations.MeannessFilterLevel>
@{
var ThisChannel = Model.Item1;
@ -91,7 +92,16 @@
</tr>
<tr>
<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>
<td colspan="2">
@ -105,9 +115,9 @@
<script type="text/javascript">
@{
var modelAsString = JsonConvert.SerializeObject(ThisChannel, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
}
const channelOnLoad = @Html.Raw(modelAsString);
function jsonifyChannel() {
@ -133,7 +143,27 @@
var tree = @Html.Raw(ViewData["channelsTree"]);
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() });
$('#accountsTree').bstreeview({ data: accountsTree() });
</script>
}

View File

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

View File

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

View File

@ -4,36 +4,40 @@
@{
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{
<script type="text/javascript">
@{
var userAsString = JsonConvert.SerializeObject(Model, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
}
const userOnLoad = @Html.Raw(userAsString);
@{
var userAsString = JsonConvert.SerializeObject(Model, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
}
const userOnLoad = @Html.Raw(userAsString);
function jsonifyUser() {
var userNow = structuredClone(userOnLoad);
userNow.Accounts = null;
@ -43,44 +47,44 @@
return userNow;
}
function getAccountsTree() {
@{
var sb = new StringBuilder();
sb.Append("[{text: \"accounts\", \"expanded\":true, nodes: [");
var first = true;
foreach (var acc in Model.Accounts)
{
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("]}]");
@{
var sb = new StringBuilder();
sb.Append("[{text: \"accounts\", \"expanded\":true, nodes: [");
var first = true;
foreach (var acc in Model.Accounts.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;
}
console.log(@Html.Raw(sb.ToString()));
sb.Append("]}]");
}
console.log(@Html.Raw(sb.ToString()));
var tree = @Html.Raw(sb.ToString());
return tree;
}
function getTagsTree() {
@{
sb = new StringBuilder();
sb.Append("[{text: \"permission tags\", \"expanded\":true, nodes: [");
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("]}]");
@{
sb = new StringBuilder();
sb.Append("[{text: \"permission tags\", \"expanded\":true, nodes: [");
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;
}
console.log(@Html.Raw(sb.ToString()));
sb.Append("]}]");
}
console.log(@Html.Raw(sb.ToString()));
var tree = @Html.Raw(sb.ToString());
return tree;
}
$('#accountsTree').bstreeview({ data: getAccountsTree() });
$('#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>
}

View File

@ -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"
;;
*)

View File

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