fixed web controller pages

This commit is contained in:
adam 2025-06-12 00:40:37 -04:00
parent a8bf8a8488
commit b859d99c92
12 changed files with 586 additions and 78 deletions

View File

@ -0,0 +1,389 @@
// <auto-generated />
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using vassago.Models;
#nullable disable
namespace vassago.Migrations
{
#pragma warning disable 612, 618, 8981
[DbContext(typeof(ChattingContext))]
[Migration("20250605145513_datamemos-more-data")]
partial class datamemosmoredata
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
modelBuilder
.HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("AccountUAC", b =>
{
b.Property<Guid>("AccountInChannelsId")
.HasColumnType("uuid");
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.HasKey("AccountInChannelsId", "UACsId");
b.HasIndex("UACsId");
b.ToTable("AccountUAC");
});
modelBuilder.Entity("ChannelUAC", b =>
{
b.Property<Guid>("ChannelsId")
.HasColumnType("uuid");
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.HasKey("ChannelsId", "UACsId");
b.HasIndex("UACsId");
b.ToTable("ChannelUAC");
});
modelBuilder.Entity("UACUser", b =>
{
b.Property<Guid>("UACsId")
.HasColumnType("uuid");
b.Property<Guid>("UsersId")
.HasColumnType("uuid");
b.HasKey("UACsId", "UsersId");
b.HasIndex("UsersId");
b.ToTable("UACUser");
});
modelBuilder.Entity("vassago.Models.Account", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("ExternalId")
.HasColumnType("text");
b.Property<bool>("IsBot")
.HasColumnType("boolean");
b.Property<Guid?>("IsUserId")
.HasColumnType("uuid");
b.Property<string>("Protocol")
.HasColumnType("text");
b.Property<Guid?>("SeenInChannelId")
.HasColumnType("uuid");
b.Property<string>("Username")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("IsUserId");
b.HasIndex("SeenInChannelId");
b.ToTable("Accounts");
});
modelBuilder.Entity("vassago.Models.Attachment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<byte[]>("Content")
.HasColumnType("bytea");
b.Property<string>("ContentType")
.HasColumnType("text");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<decimal?>("ExternalId")
.HasColumnType("numeric(20,0)");
b.Property<string>("Filename")
.HasColumnType("text");
b.Property<Guid?>("MessageId")
.HasColumnType("uuid");
b.Property<int>("Size")
.HasColumnType("integer");
b.Property<string>("Source")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("MessageId");
b.ToTable("Attachments");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<int>("ChannelType")
.HasColumnType("integer");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<string>("ExternalId")
.HasColumnType("text");
b.Property<int?>("LewdnessFilterLevel")
.HasColumnType("integer");
b.Property<bool?>("LinksAllowed")
.HasColumnType("boolean");
b.Property<decimal?>("MaxAttachmentBytes")
.HasColumnType("numeric(20,0)");
b.Property<long?>("MaxTextChars")
.HasColumnType("bigint");
b.Property<int?>("MeannessFilterLevel")
.HasColumnType("integer");
b.Property<Guid?>("ParentChannelId")
.HasColumnType("uuid");
b.Property<string>("Protocol")
.HasColumnType("text");
b.Property<bool?>("ReactionsPossible")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("ParentChannelId");
b.ToTable("Channels");
});
modelBuilder.Entity("vassago.Models.Message", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("ActedOn")
.HasColumnType("boolean");
b.Property<Guid?>("AuthorId")
.HasColumnType("uuid");
b.Property<Guid?>("ChannelId")
.HasColumnType("uuid");
b.Property<string>("Content")
.HasColumnType("text");
b.Property<string>("ExternalId")
.HasColumnType("text");
b.Property<bool>("MentionsMe")
.HasColumnType("boolean");
b.Property<string>("Protocol")
.HasColumnType("text");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("AuthorId");
b.HasIndex("ChannelId");
b.ToTable("Messages");
});
modelBuilder.Entity("vassago.Models.UAC", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Dictionary<string, string>>("CommandAliases")
.HasColumnType("hstore");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<Dictionary<string, string>>("Localization")
.HasColumnType("hstore");
b.Property<Guid>("OwnerId")
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("UACs");
});
modelBuilder.Entity("vassago.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("AccountUAC", b =>
{
b.HasOne("vassago.Models.Account", null)
.WithMany()
.HasForeignKey("AccountInChannelsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ChannelUAC", b =>
{
b.HasOne("vassago.Models.Channel", null)
.WithMany()
.HasForeignKey("ChannelsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("UACUser", b =>
{
b.HasOne("vassago.Models.UAC", null)
.WithMany()
.HasForeignKey("UACsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("vassago.Models.User", null)
.WithMany()
.HasForeignKey("UsersId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("vassago.Models.Account", b =>
{
b.HasOne("vassago.Models.User", "IsUser")
.WithMany("Accounts")
.HasForeignKey("IsUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("vassago.Models.Channel", "SeenInChannel")
.WithMany("Users")
.HasForeignKey("SeenInChannelId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("IsUser");
b.Navigation("SeenInChannel");
});
modelBuilder.Entity("vassago.Models.Attachment", b =>
{
b.HasOne("vassago.Models.Message", "Message")
.WithMany("Attachments")
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Message");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.HasOne("vassago.Models.Channel", "ParentChannel")
.WithMany("SubChannels")
.HasForeignKey("ParentChannelId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("ParentChannel");
});
modelBuilder.Entity("vassago.Models.Message", b =>
{
b.HasOne("vassago.Models.Account", "Author")
.WithMany()
.HasForeignKey("AuthorId");
b.HasOne("vassago.Models.Channel", "Channel")
.WithMany("Messages")
.HasForeignKey("ChannelId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Author");
b.Navigation("Channel");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.Navigation("Messages");
b.Navigation("SubChannels");
b.Navigation("Users");
});
modelBuilder.Entity("vassago.Models.Message", b =>
{
b.Navigation("Attachments");
});
modelBuilder.Entity("vassago.Models.User", b =>
{
b.Navigation("Accounts");
});
#pragma warning restore 612, 618, 8981
}
}
}

View File

@ -0,0 +1,55 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace vassago.Migrations
{
#pragma warning disable 8981
/// <inheritdoc />
public partial class datamemosmoredata : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Dictionary<string, string>>(
name: "CommandAliases",
table: "UACs",
type: "hstore",
nullable: true);
migrationBuilder.AddColumn<Dictionary<string, string>>(
name: "Localization",
table: "UACs",
type: "hstore",
nullable: true);
//NOTE for future me: migrationBuilder.SQL("SELECT localization INTO Aliases from Channels;");, but also make the rows for it.
//too lazy now, really leaning on the "this will work fine for my 0 users"
migrationBuilder.DropColumn(
name: "Aliases",
table: "Channels");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Dictionary<string, string>>(
name: "Aliases",
table: "Channels",
type: "hstore",
nullable: true);
migrationBuilder.DropColumn(
name: "CommandAliases",
table: "UACs");
migrationBuilder.DropColumn(
name: "Localization",
table: "UACs");
}
}
}
#pragma warning restore 8981

View File

@ -148,9 +148,6 @@ namespace vassago.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Dictionary<string, string>>("Aliases")
.HasColumnType("hstore");
b.Property<int>("ChannelType")
.HasColumnType("integer");
@ -236,12 +233,18 @@ namespace vassago.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Dictionary<string, string>>("CommandAliases")
.HasColumnType("hstore");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<Dictionary<string, string>>("Localization")
.HasColumnType("hstore");
b.Property<Guid>("OwnerId")
.HasColumnType("uuid");

View File

@ -36,8 +36,6 @@ public class Channel
public Enumerations.LewdnessFilterLevel? LewdnessFilterLevel { get; set; }
public Enumerations.MeannessFilterLevel? MeannessFilterLevel { get; set; }
public List<UAC> UACs { get; set; }
//both incoming and outgoing
//public Dictionary<string, string> Aliases { get; set; }
public DefinitePermissionSettings EffectivePermissions
{

View File

@ -5,6 +5,11 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using vassago.Models;
//TODO: rename.
//"uac" originally meant "user account control". but it might just be channel control. in fact, channel-control is much more fun,
//then the platform manages the permissions for you!
//but now I'm going to add locales to it, so it's kind of... "miscellaneous attached data". Official Sticky Notes, if you will.
public class UAC
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
@ -26,4 +31,7 @@ public class UAC
///it's variably before and after compile time. shrug.emote.
///</summary>
public string Description { get; set; }
public Dictionary<string, string> CommandAliases {get; set;}
public Dictionary<string, string> Localization {get; set;}
}

View File

@ -15,18 +15,23 @@ public static class Rememberer
{
dbAccessSemaphore.Wait();
channels = db.Channels.ToList();
Console.WriteLine($"caching channels. {channels.Count} channels retrieved");
foreach (Channel ch in channels)
{
if (ch.ParentChannelId != null)
{
ch.ParentChannel = channels.FirstOrDefault(c => c.Id == ch.ParentChannelId);
ch.ParentChannel.SubChannels ??= [];
if (!ch.ParentChannel.SubChannels.Contains(ch))
{
ch.ParentChannel.SubChannels.Add(ch);
}
}
if (ch.Messages?.Count > 0)
{
Console.WriteLine($"{ch.DisplayName} got {ch.Messages.Count} messages");
}
ch.SubChannels ??= [];
}
channelCacheDirty = false;
dbAccessSemaphore.Release();
@ -179,6 +184,8 @@ public static class Rememberer
db.Channels.Remove(toForget);
db.SaveChanges();
dbAccessSemaphore.Release();
channelCacheDirty = true;
cacheChannels();
}
public static void ForgetMessage(Message toForget)
{
@ -211,11 +218,9 @@ public static class Rememberer
}
public static List<Channel> ChannelsOverview()
{
List<Channel> toReturn;
dbAccessSemaphore.Wait();
toReturn = [.. db.Channels.Include(u => u.SubChannels).Include(c => c.ParentChannel)];
dbAccessSemaphore.Release();
return toReturn;
if (channelCacheDirty)
Task.Run(() => cacheChannels()).Wait();
return channels;
}
public static Account AccountDetail(Guid Id)
{
@ -318,5 +323,7 @@ public static class Rememberer
db.Update(toRemember);
db.SaveChanges();
dbAccessSemaphore.Release();
if (toRemember.Channels?.Count() > 0)
cacheChannels();
}
}

View File

@ -14,16 +14,12 @@ public class ChannelsController() : Controller
{
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.
return Problem("no channels.");
var channel = allChannels.FirstOrDefault(u => u.Id == id);
if (channel == null)
{
return Problem("couldn't find that channle");
return Problem($"couldn't find channle {id}");
}
var walker = channel;
while (walker != null)
@ -50,7 +46,7 @@ public class ChannelsController() : Controller
}
sb.Append("]}]");
ViewData.Add("channelsTree", sb.ToString());
ViewData.Add("subChannelsTree", sb.ToString());
return View(
new Tuple<Channel, Enumerations.LewdnessFilterLevel, Enumerations.MeannessFilterLevel>(
channel, channel.EffectivePermissions.LewdnessFilterLevel, channel.EffectivePermissions.MeannessFilterLevel

View File

@ -43,7 +43,12 @@ public class HomeController : Controller
{
sb.Append(',');
}
sb.Append($"{{\"text\": \"<a href=\\\"{Url.ActionLink(action: "Details", controller: "UACs", values: new {id = uac.Id})}\\\">{uac.DisplayName}</a>\"}}");
var displayedName = uac.DisplayName;
if(string.IsNullOrWhiteSpace(displayedName))
{
displayedName = $"[unnamed - {uac.Id}]";
}
sb.Append($"{{\"text\": \"<a href=\\\"{Url.ActionLink(action: "Details", controller: "UACs", values: new {id = uac.Id})}\\\">{displayedName}</a>\"}}");
}
sb.Append("]}");
}
@ -78,7 +83,7 @@ public class HomeController : Controller
//type error, e is not defined
//channels
sb.Append(",{text: \"channels\", expanded:true, nodes: [");
var topLevelChannels = Rememberer.ChannelsOverview().Where(x => x.ParentChannel == null);
var topLevelChannels = Rememberer.ChannelsOverview().Where(x => x.ParentChannel == null).ToList();
first = true;
foreach (var topLevelChannel in topLevelChannels)
{

View File

@ -210,4 +210,23 @@ public class UACController: ControllerBase
Rememberer.RememberUAC(uacFromDb);
return Ok(uacFromDb);
}
[HttpPut]
[Route("CreateForChannels/{Id}")]
[Produces("application/json")]
public IActionResult CreateForChannels(Guid Id)
{
_logger.LogDebug($"made it to controller. creating for channel {Id}");
var targetChannel = Rememberer.ChannelDetail(Id);
if (targetChannel == null)
{
return NotFound();
}
var newUAC = new UAC()
{
Channels = [targetChannel]
};
Rememberer.RememberUAC(newUAC);
Rememberer.RememberChannel(targetChannel);
return Ok(newUAC.Id);
}
}

View File

@ -103,6 +103,12 @@
}
</td>
</tr>
<tr>
<th scope="row">Datamemos</th>
<td>
<div id="dataMemosTree"></div>
</td>
</tr>
<tr>
<td colspan="2">
<button onclick="forget()">forget</button>
@ -138,15 +144,49 @@
deleteModel(jsonifyChannel().Id, window.history.back);
}
}
function createMemo()
{
console.log("creating memo for channel..");
createMemoFor((newMemoId) => {
window.location.href = "/UACs/Details/" + newMemoId;
});
}
function channelsTree() {
var tree = @Html.Raw(ViewData["channelsTree"]);
//TOOD: see how accountsTree does all our HTML-ification over here in HTML land? but this doesn't? we should pick one and stick with it.
var tree = @Html.Raw(ViewData["subChannelsTree"]);
return tree;
}
function dataMemosTree(){
@{
var dmsb = new StringBuilder();
dmsb.Append("[{text: \"Data Memos\", \"expanded\":true, nodes: [");
var firstMemo = true;
if(ThisChannel.UACs != null) foreach(var memo in ThisChannel.UACs)
{
if(!firstMemo)
dmsb.Append(',');
var effectiveDisplayName = memo.DisplayName;
if(string.IsNullOrWhiteSpace(effectiveDisplayName))
{
effectiveDisplayName = $"[nameless] {memo.Id}";
}
dmsb.Append($"{{text: \"<a href=\\\"/UACs/Details/{memo.Id}\\\">{effectiveDisplayName}</a>\"}}");
firstMemo = false;
}
if(!firstMemo)
dmsb.Append(',');
dmsb.Append($"{{text: \"<button type=\\\"button\\\" class=\\\"btn btn-primary\\\" onclick=\\\"createMemo()\\\">+</button>\"}}");
dmsb.Append("]}]");
}
var tree = @Html.Raw(dmsb.ToString());
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))
@ -157,6 +197,7 @@
first=false;
}
sb.Append("]}]");
}
//console.log(@Html.Raw(sb.ToString()));
var tree = @Html.Raw(sb.ToString());
@ -164,6 +205,7 @@
}
$('#channelsTree').bstreeview({ data: channelsTree() });
$('#accountsTree').bstreeview({ data: accountsTree() });
$('#dataMemosTree').bstreeview({ data: dataMemosTree() });
</script>
}

View File

@ -1,42 +0,0 @@
@model IEnumerable<Channel>
@{
ViewData["Title"] = "Channels";
}
<table class="table">
<thead>
<tr>
<th>
protocol
</th>
<th>type</th>
<th>
display name
</th>
<th>
Lineage
</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td class="@item.Protocol">
<div class="protocol-icon">&nbsp;</div>
</td>
<td class="@item.ChannelType">
<div class="channel-type-icon">&nbsp;</div>
</td>
<td>
@Html.DisplayFor(modelItem => item.DisplayName)
</td>
<td>
@item.LineageSummary
</td>
<td>
<a asp-action="Details" asp-route-id="@item.Id">Details</a>
</td>
</tr>
}
</tbody>
</table>

View File

@ -49,7 +49,7 @@ function deleteModel(id, callback)
var components = window.location.pathname.split('/');
var type=components[1];
let result = null;
var id=components[3];
var id=components[3]; //wait... i send it the ID then overwrite it? lmao what? TODO: fix
fetch(apiUrl + 'Rememberer/' + type + '/' + id, {
method: 'DELETE',
headers: {
@ -71,9 +71,37 @@ function deleteModel(id, callback)
console.error('Error:', error);
});
}
function createMemoFor(callback)
{
let components = window.location.pathname.split('/');
let type=components[1];
type = type.charAt(0).toUpperCase() + type.slice(1).toLowerCase();
let result = null;
let id=components[3];
console.log("createMemoFor" + type + "(" + id + ")");
fetch(apiUrl + "UAC/CreateFor" + type + "/" + id, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not "ok". which is not ok.');
}
return response.json();
})
.then(returnedSuccessdata => {
console.log("success");
console.log('returnedSuccessdata:', returnedSuccessdata);
if(callback !== null) { callback(returnedSuccessdata); }
})
.catch(error => {
console.error('Error:', error);
});
}
function linkUAC_Channel(channel_guid, callback)
{
var components = window.location.pathname.split('/');
var id=components[3];
let model={"uac_guid": id,