forked from adam/discord-bot-shtik
Compare commits
14 Commits
6298b037b6
...
4e82eedf9c
Author | SHA1 | Date | |
---|---|---|---|
4e82eedf9c | |||
b6f74f580c | |||
488a89614a | |||
d22faae2f6 | |||
6881816c94 | |||
53753374f0 | |||
50ecfc5867 | |||
0d3a56c8db | |||
18e8f0f36e | |||
d006367ecc | |||
c971add137 | |||
3ed37959ad | |||
736e3cb763 | |||
740471d105 |
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -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,
|
||||
|
45
Behaver.cs
45
Behaver.cs
@ -8,6 +8,7 @@ using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using vassago.ProtocolInterfaces.DiscordInterface;
|
||||
|
||||
public class Behaver
|
||||
{
|
||||
@ -72,55 +73,31 @@ public class Behaver
|
||||
|
||||
public void MarkSelf(Account selfAccount)
|
||||
{
|
||||
var db = new ChattingContext();
|
||||
if(SelfUser == null)
|
||||
{
|
||||
SelfUser = selfAccount.IsUser;
|
||||
}
|
||||
else if (SelfUser != selfAccount.IsUser)
|
||||
{
|
||||
CollapseUsers(SelfUser, selfAccount.IsUser, db);
|
||||
CollapseUsers(SelfUser, selfAccount.IsUser);
|
||||
}
|
||||
SelfAccounts = db.Accounts.Where(a => a.IsUser == SelfUser).ToList();
|
||||
db.SaveChanges();
|
||||
SelfAccounts = Rememberer.SearchAccounts(a => a.IsUser == SelfUser);
|
||||
Rememberer.RememberAccount(selfAccount);
|
||||
}
|
||||
|
||||
public bool CollapseUsers(User primary, User secondary, ChattingContext db)
|
||||
public bool CollapseUsers(User primary, User secondary)
|
||||
{
|
||||
Console.WriteLine($"{secondary.Id} is being consumed into {primary.Id}");
|
||||
primary.Accounts.AddRange(secondary.Accounts);
|
||||
if(primary.Accounts == null)
|
||||
primary.Accounts = new List<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;
|
||||
}
|
||||
}
|
||||
|
@ -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 :)");
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;}
|
||||
}
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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))
|
||||
|
@ -7,7 +7,7 @@ using Discord.WebSocket;
|
||||
using Discord;
|
||||
using Discord.Net;
|
||||
|
||||
namespace vassago.DiscordInterface
|
||||
namespace vassago.ProtocolInterfaces.DiscordInterface
|
||||
{
|
||||
public static class SlashCommandsHelper
|
||||
{
|
||||
|
10
README.md
10
README.md
@ -11,6 +11,16 @@ that's read messages/view channels, send messages, send messages in threads, and
|
||||
|
||||
## Data Types
|
||||
|
||||
database diagram. is a fancy term.
|
||||
|
||||
message 1:n attachment
|
||||
user 1:n account
|
||||
channel 1:n account
|
||||
channel 1:n message
|
||||
account 1:n message
|
||||
|
||||
featurepermission n:n ?
|
||||
|
||||
### Accounts
|
||||
|
||||
a `User` can have multiple `Account`s. e.g., @adam:greyn.club? that's an "account". I, however, am a `User`. An `Account` has references to the `Channels` its seen in - as in, leaf-level. If you're in a subchannel, you'll have an appropriate listing there - i.e., you will never have an account in "discord (itself)", you'll have one in the guild text-channels
|
||||
|
110
Rememberer.cs
Normal file
110
Rememberer.cs
Normal 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();
|
||||
}
|
||||
}
|
35
WebInterface/Controllers/AccountsController.cs
Normal file
35
WebInterface/Controllers/AccountsController.cs
Normal 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 });
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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>\", ");
|
||||
|
@ -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.");
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
40
WebInterface/Controllers/api/UsersController.cs
Normal file
40
WebInterface/Controllers/api/UsersController.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
68
WebInterface/Views/Accounts/Details.cshtml
Normal file
68
WebInterface/Views/Accounts/Details.cshtml
Normal 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"> </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>
|
||||
}
|
@ -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\\\"> </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>
|
||||
}
|
@ -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">
|
||||
|
@ -1,4 +1,4 @@
|
||||
@model ErrorPageViewModel
|
||||
@model vassago.WebInterface.Models.ErrorPageViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
|
@ -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\\\"> </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\\\"> </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>
|
||||
}
|
||||
|
@ -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"
|
||||
;;
|
||||
*)
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user