Compare commits

..

3 Commits

Author SHA1 Message Date
66b425cd39 distinguish users and accounts 2023-06-19 12:56:40 -04:00
e433e56fec Permissions associated with channel 2023-06-19 11:03:06 -04:00
a1d2ec83b5 no onjoin, who cares 2023-06-19 01:34:56 -04:00
32 changed files with 887 additions and 139 deletions

View File

@ -34,18 +34,15 @@ public class Behaver
public async Task<bool> ActOn(Message message) public async Task<bool> ActOn(Message message)
{ {
var permissions = new PermissionSettings(); //TODO: get combined permissions for author and channel
var contentWithoutMention = message.Content;
foreach (var behavior in behaviors) foreach (var behavior in behaviors)
{ {
if (behavior.ShouldAct(permissions, message)) if (behavior.ShouldAct(message))
{ {
behavior.ActOn(permissions, message); behavior.ActOn(message);
message.ActedOn = true; message.ActedOn = true;
} }
} }
if (message.ActedOn == false && message.MentionsMe && contentWithoutMention.Contains('?')) if (message.ActedOn == false && message.MentionsMe && message.Content.Contains('?'))
{ {
Console.WriteLine("providing bullshit nonanswer / admitting uselessness"); Console.WriteLine("providing bullshit nonanswer / admitting uselessness");
var responses = new List<string>(){ var responses = new List<string>(){
@ -61,10 +58,5 @@ public class Behaver
} }
return message.ActedOn; return message.ActedOn;
} }
internal Task OnJoin(User u, Channel defaultChannel)
{
throw new NotImplementedException();
}
} }
#pragma warning restore 4014 //the "async not awaited" error #pragma warning restore 4014 //the "async not awaited" error

View File

@ -11,10 +11,9 @@ using System.Collections.Generic;
//expect a behavior to be created per mesage //expect a behavior to be created per mesage
public abstract class Behavior public abstract class Behavior
{ {
//TODO: message should have a channel, which should provide permissions. shouldn't have to pass it here. public abstract Task<bool> ActOn(Message message);
public abstract Task<bool> ActOn(PermissionSettings permissions, Message message);
public virtual bool ShouldAct(PermissionSettings permissions, Message message) public virtual bool ShouldAct(Message message)
{ {
return Regex.IsMatch(message.Content, $"{Trigger}\\b", RegexOptions.IgnoreCase); return Regex.IsMatch(message.Content, $"{Trigger}\\b", RegexOptions.IgnoreCase);
} }

View File

@ -16,7 +16,7 @@ public class ChatGPTSnark : Behavior
public override string Description => "snarkiness about the latest culty-fixation in ai"; public override string Description => "snarkiness about the latest culty-fixation in ai";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
await message.Channel.SendMessage("chatGPT is **weak**. also, are we done comparing every little if-then-else to skynet?"); await message.Channel.SendMessage("chatGPT is **weak**. also, are we done comparing every little if-then-else to skynet?");
return true; return true;

View File

@ -16,7 +16,7 @@ public class DefinitionSnarkCogDiss : Behavior
public override string Description => "snarkiness about the rampant misuse of the term cognitive dissonance"; public override string Description => "snarkiness about the rampant misuse of the term cognitive dissonance";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
await message.Reply("that's not what cognitive dissonance means. Did you mean \"hypocrisy\"?"); await message.Reply("that's not what cognitive dissonance means. Did you mean \"hypocrisy\"?");
return true; return true;

View File

@ -16,7 +16,7 @@ public class DefinitionSnarkGaslight : Behavior
public override string Description => "snarkiness about the rampant misuse of the term gaslighting"; public override string Description => "snarkiness about the rampant misuse of the term gaslighting";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
await message.Channel.SendMessage("that's not what gaslight means. Did you mean \"say something that (you believe) is wrong\"?"); await message.Channel.SendMessage("that's not what gaslight means. Did you mean \"say something that (you believe) is wrong\"?");
return true; return true;

View File

@ -22,9 +22,9 @@ public class Detiktokify : Behavior
ytdl.OutputFolder = ""; ytdl.OutputFolder = "";
ytdl.OutputFileTemplate = "tiktokbad.%(ext)s"; ytdl.OutputFileTemplate = "tiktokbad.%(ext)s";
} }
public override bool ShouldAct(PermissionSettings permissions, Message message) public override bool ShouldAct(Message message)
{ {
if(permissions.MaxAttachmentBytes == 0) if(message.Channel.EffectivePermissions.MaxAttachmentBytes == 0)
return false; return false;
var wordLikes = message.Content.Split(' ', StringSplitOptions.TrimEntries); var wordLikes = message.Content.Split(' ', StringSplitOptions.TrimEntries);
@ -41,16 +41,17 @@ public class Detiktokify : Behavior
} }
return tiktokLinks.Any(); return tiktokLinks.Any();
} }
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
foreach(var link in tiktokLinks) foreach(var link in tiktokLinks)
{ {
#pragma warning disable 4014
message.React("tiktokbad");
#pragma warning restore 4014
try try
{ {
Console.WriteLine("detiktokifying");
#pragma warning disable 4014
await message.React("<:tiktok:1070038619584200884>");
#pragma warning restore 4014
var res = await ytdl.RunVideoDownload(link.ToString()); var res = await ytdl.RunVideoDownload(link.ToString());
if (!res.Success) if (!res.Success)
{ {
@ -64,7 +65,7 @@ public class Detiktokify : Behavior
if (File.Exists(path)) if (File.Exists(path))
{ {
var bytesize = new System.IO.FileInfo(path).Length; var bytesize = new System.IO.FileInfo(path).Length;
if (bytesize < permissions.MaxAttachmentBytes - 256) if (bytesize < message.Channel.EffectivePermissions.MaxAttachmentBytes - 256)
{ {
try try
{ {

View File

@ -18,7 +18,7 @@ public class FiximageHeic : Behavior
public override string Description => "convert heic images to jpg"; public override string Description => "convert heic images to jpg";
private List<Attachment> heics = new List<Attachment>(); private List<Attachment> heics = new List<Attachment>();
public override bool ShouldAct(PermissionSettings permissions, Message message) public override bool ShouldAct(Message message)
{ {
if (message.Attachments?.Count() > 0) if (message.Attachments?.Count() > 0)
{ {
@ -33,7 +33,7 @@ public class FiximageHeic : Behavior
return heics.Any(); return heics.Any();
} }
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
if (!Directory.Exists("tmp")) if (!Directory.Exists("tmp"))
{ {

View File

@ -15,13 +15,13 @@ public class GeneralSnarkCloudNative : Behavior
public override string Name => "general snarkiness: cloud native"; public override string Name => "general snarkiness: cloud native";
public override string Trigger => "certain tech buzzwords that no human uses in normal conversation"; public override string Trigger => "certain tech buzzwords that no human uses in normal conversation";
public override bool ShouldAct(PermissionSettings permissions, Message message) public override bool ShouldAct(Message message)
{ {
return Regex.IsMatch(message.Content, "\\bcloud( |-)?native\\b", RegexOptions.IgnoreCase) || return Regex.IsMatch(message.Content, "\\bcloud( |-)?native\\b", RegexOptions.IgnoreCase) ||
Regex.IsMatch(message.Content, "\\benterprise( |-)?(level|solution)\\b", RegexOptions.IgnoreCase); Regex.IsMatch(message.Content, "\\benterprise( |-)?(level|solution)\\b", RegexOptions.IgnoreCase);
} }
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
switch (Shared.r.Next(2)) switch (Shared.r.Next(2))
{ {

View File

@ -17,11 +17,11 @@ public class GeneralSnarkPlaying : Behavior
public override string Description => "I didn't think you were playing, but now I do"; public override string Description => "I didn't think you were playing, but now I do";
public override bool ShouldAct(PermissionSettings permissions, Message message) public override bool ShouldAct(Message message)
{ {
return Regex.IsMatch(message.Content, "^(s?he|(yo)?u|y'?all|they) thinks? i'?m (playin|jokin|kiddin)g?$", RegexOptions.IgnoreCase); return Regex.IsMatch(message.Content, "^(s?he|(yo)?u|y'?all|they) thinks? i'?m (playin|jokin|kiddin)g?$", RegexOptions.IgnoreCase);
} }
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
await message.Channel.SendMessage("I believed you for a second, but then you assured me you's a \uD83C\uDDE7 \uD83C\uDDEE \uD83C\uDDF9 \uD83C\uDDE8 \uD83C\uDDED"); await message.Channel.SendMessage("I believed you for a second, but then you assured me you's a \uD83C\uDDE7 \uD83C\uDDEE \uD83C\uDDF9 \uD83C\uDDE8 \uD83C\uDDED");
return true; return true;

View File

@ -16,7 +16,7 @@ public class GeneralSnarkSkynet : Behavior
public override string Description => "snarkiness about the old AI fixation"; public override string Description => "snarkiness about the old AI fixation";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
switch (Shared.r.Next(5)) switch (Shared.r.Next(5))
{ {

View File

@ -15,11 +15,11 @@ public class Gratitude : Behavior
public override string Trigger => "thank me"; public override string Trigger => "thank me";
public override bool ShouldAct(PermissionSettings permissions, Message message) public override bool ShouldAct(Message message)
{ {
return Regex.IsMatch(message.Content, "\\bthank (yo)?u\\b", RegexOptions.IgnoreCase) && message.MentionsMe; return Regex.IsMatch(message.Content, "\\bthank (yo)?u\\b", RegexOptions.IgnoreCase) && message.MentionsMe;
} }
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
switch (Shared.r.Next(4)) switch (Shared.r.Next(4))

View File

@ -17,7 +17,7 @@ public class Joke : Behavior
public override string Description => "tell a joke"; public override string Description => "tell a joke";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
Console.WriteLine("joking"); Console.WriteLine("joking");
var jokes = File.ReadAllLines("assets/jokes.txt"); var jokes = File.ReadAllLines("assets/jokes.txt");
@ -37,12 +37,12 @@ public class Joke : Behavior
var punchline = thisJoke.Substring(firstIndexAfterQuestionMark, thisJoke.Length - firstIndexAfterQuestionMark); var punchline = thisJoke.Substring(firstIndexAfterQuestionMark, thisJoke.Length - firstIndexAfterQuestionMark);
Task.WaitAll(message.Channel.SendMessage(straightline)); Task.WaitAll(message.Channel.SendMessage(straightline));
Thread.Sleep(TimeSpan.FromSeconds(Shared.r.Next(5, 30))); Thread.Sleep(TimeSpan.FromSeconds(Shared.r.Next(5, 30)));
await message.Channel.SendMessage(punchline); //if (Shared.r.Next(8) == 0)
// var myOwnMsg = await message.Channel.SendMessage(punchline);
if (Shared.r.Next(8) == 0)
{ {
LaughAtOwnJoke.punchlinesAwaitingReaction.Add(punchline); LaughAtOwnJoke.punchlinesAwaitingReaction.Add(punchline);
} }
await message.Channel.SendMessage(punchline);
// var myOwnMsg = await message.Channel.SendMessage(punchline);
}); });
#pragma warning restore 4014 #pragma warning restore 4014
} }

View File

@ -18,14 +18,15 @@ public class LaughAtOwnJoke : Behavior
public override string Description => Name; public override string Description => Name;
public static List<string> punchlinesAwaitingReaction = new List<string>(); public static List<string> punchlinesAwaitingReaction = new List<string>();
public override bool ShouldAct(PermissionSettings permissions, Message message) public override bool ShouldAct(Message message)
{ {
//TODO: i need to keep track of myself from here somehow //TODO: i need to keep track of myself from here somehow
return false; //return false;
//return message.Author == me && punchlinesAwaitingReaction.Contains(message.Content); return /*message.Author == me &&*/ punchlinesAwaitingReaction.Contains(message.Content);
} }
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
punchlinesAwaitingReaction.Remove(message.Content); punchlinesAwaitingReaction.Remove(message.Content);
await message.React("\U0001F60E"); //smiling face with sunglasses await message.React("\U0001F60E"); //smiling face with sunglasses

View File

@ -18,7 +18,7 @@ public class PepTalk : Behavior
public override string Description => "assembles a pep talk from a few pieces"; public override string Description => "assembles a pep talk from a few pieces";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{var piece1 = new List<string>{ {var piece1 = new List<string>{
"Champ, ", "Champ, ",
"Fact: ", "Fact: ",

View File

@ -14,7 +14,7 @@ public class PulseCheck : Behavior
public override string Trigger => "!pluse ?check"; public override string Trigger => "!pluse ?check";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
await message.Channel.SendFile("assets/ekgblip.png", null); await message.Channel.SendFile("assets/ekgblip.png", null);
return true; return true;

View File

@ -18,7 +18,7 @@ public class QRify : Behavior
public override string Description => "generate text QR codes"; public override string Description => "generate text QR codes";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
var qrContent = message.Content.Substring($"{Trigger} ".Length + message.Content.IndexOf(Trigger)); var qrContent = message.Content.Substring($"{Trigger} ".Length + message.Content.IndexOf(Trigger));
Console.WriteLine($"qring: {qrContent}"); Console.WriteLine($"qring: {qrContent}");

View File

@ -10,7 +10,7 @@ public class UnitConvert : Behavior
public override string Trigger => "!freedomunits"; public override string Trigger => "!freedomunits";
public override string Description => "convert between many units."; public override string Description => "convert between many units.";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
var theseMatches = Regex.Matches(message.Content, "\\b([\\d]+\\.?\\d*) ?([^\\d\\s].*) (in|to|as) ([^\\d\\s].*)$", RegexOptions.IgnoreCase); var theseMatches = Regex.Matches(message.Content, "\\b([\\d]+\\.?\\d*) ?([^\\d\\s].*) (in|to|as) ([^\\d\\s].*)$", RegexOptions.IgnoreCase);

View File

@ -16,7 +16,7 @@ public class WishLuck : Behavior
public override string Description => "wishes you luck"; public override string Description => "wishes you luck";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message) public override async Task<bool> ActOn(Message message)
{ {
if (Shared.r.Next(20) == 0) if (Shared.r.Next(20) == 0)
{ {

View File

@ -18,6 +18,15 @@ public class DiscordInterface
internal DiscordSocketClient client; internal DiscordSocketClient client;
private bool eventsSignedUp = false; private bool eventsSignedUp = false;
private ChattingContext _db; private ChattingContext _db;
private static PermissionSettings defaultPermissions = new PermissionSettings()
{
MeannessFilterLevel = 1,
LewdnessFilterLevel = 3,
MaxTextChars = 2000,
MaxAttachmentBytes = 8 * 1024 * 1024,
LinksAllowed = true,
ReactionsPossible = true
};
public DiscordInterface() public DiscordInterface()
{ {
_db = Shared.dbContext; _db = Shared.dbContext;
@ -38,11 +47,11 @@ public class DiscordInterface
if (!eventsSignedUp) if (!eventsSignedUp)
{ {
eventsSignedUp = true; eventsSignedUp = true;
Console.WriteLine("Bot is connected! going to sign up for message received and user joined in client ready"); Console.WriteLine($"Bot is connected {client.CurrentUser.Mention}! going to sign up for message received and user joined in client ready");
client.MessageReceived += MessageReceived; client.MessageReceived += MessageReceived;
// _client.MessageUpdated += // _client.MessageUpdated +=
client.UserJoined += UserJoined; // client.UserJoined += UserJoined;
client.SlashCommandExecuted += SlashCommandHandler; client.SlashCommandExecuted += SlashCommandHandler;
// _client.ChannelCreated += // _client.ChannelCreated +=
// _client.ChannelDestroyed += // _client.ChannelDestroyed +=
@ -90,7 +99,7 @@ public class DiscordInterface
m.MentionsMe = true; m.MentionsMe = true;
} }
if ((suMessage.Author.Id != client.CurrentUser.Id)) // if ((suMessage.Author.Id != client.CurrentUser.Id))
{ {
if (await Behaver.Instance.ActOn(m)) if (await Behaver.Instance.ActOn(m))
{ {
@ -100,13 +109,13 @@ public class DiscordInterface
_db.SaveChanges(); _db.SaveChanges();
} }
private Task UserJoined(SocketGuildUser arg) private void UserJoined(SocketGuildUser arg)
{ {
var guild = UpsertChannel(arg.Guild); var guild = UpsertChannel(arg.Guild);
var defaultChannel = UpsertChannel(arg.Guild.DefaultChannel); var defaultChannel = UpsertChannel(arg.Guild.DefaultChannel);
defaultChannel.ParentChannel = guild; defaultChannel.ParentChannel = guild;
var u = UpsertUser(arg); var u = UpsertUser(arg);
return Behaver.Instance.OnJoin(u, defaultChannel);
} }
private async Task ButtonHandler(SocketMessageComponent component) private async Task ButtonHandler(SocketMessageComponent component)
{ {
@ -189,6 +198,7 @@ public class DiscordInterface
m.ExternalId = dMessage.Id; m.ExternalId = dMessage.Id;
m.Timestamp = dMessage.EditedTimestamp ?? dMessage.CreatedAt; m.Timestamp = dMessage.EditedTimestamp ?? dMessage.CreatedAt;
if (dMessage.MentionedUserIds?.FirstOrDefault(muid => muid == client.CurrentUser.Id) != null) if (dMessage.MentionedUserIds?.FirstOrDefault(muid => muid == client.CurrentUser.Id) != null)
{ {
m.MentionsMe = true; m.MentionsMe = true;
@ -199,9 +209,26 @@ public class DiscordInterface
} }
m.Reply = (t) => { return dMessage.ReplyAsync(t); }; m.Reply = (t) => { return dMessage.ReplyAsync(t); };
m.React = (e) => { return dMessage.AddReactionAsync(Emote.Parse(e)); }; m.React = (e) => { return attemptReact(dMessage, e); };
return m; return m;
} }
private Task attemptReact(IUserMessage msg, string e)
{
var c = _db.Channels.FirstOrDefault(c => c.ExternalId == msg.Channel.Id);
//var preferredEmote = c.EmoteOverrides?[e] ?? e; //TODO: emote overrides
var preferredEmote = e;
Emote emote;
if(!Emote.TryParse(preferredEmote, out emote))
{
if(preferredEmote == e)
Console.Error.WriteLine($"never heard of emote {e}");
return null;
}
return msg.AddReactionAsync(emote);
}
internal Channel UpsertChannel(IMessageChannel channel) internal Channel UpsertChannel(IMessageChannel channel)
{ {
var addPlease = false; var addPlease = false;
@ -267,14 +294,14 @@ public class DiscordInterface
c.SendFile = (f, t) => { throw new InvalidOperationException($"channel {channel.Name} is guild; send file"); }; c.SendFile = (f, t) => { throw new InvalidOperationException($"channel {channel.Name} is guild; send file"); };
return c; return c;
} }
internal User UpsertUser(IUser user) internal Account UpsertUser(IUser user)
{ {
var addPlease = false; var addPlease = false;
var u = _db.Users.FirstOrDefault(ui => ui.ExternalId == user.Id); var u = _db.Users.FirstOrDefault(ui => ui.ExternalId == user.Id);
if (u == null) if (u == null)
{ {
addPlease = true; addPlease = true;
u = new User(); u = new Account();
} }
u.Username = user.Username; u.Username = user.Username;
u.ExternalId = user.Id; u.ExternalId = user.Id;

View File

@ -23,6 +23,7 @@ namespace vassago.DiscordInterface
}; };
public static async Task Register(DiscordSocketClient client) public static async Task Register(DiscordSocketClient client)
{ {
return;
var commandsInContext = await client.GetGlobalApplicationCommandsAsync(); var commandsInContext = await client.GetGlobalApplicationCommandsAsync();
await Register(client, commandsInContext, null); await Register(client, commandsInContext, null);
foreach (var guild in client.Guilds) foreach (var guild in client.Guilds)

View File

@ -0,0 +1,255 @@
// <auto-generated />
using System;
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
{
[DbContext(typeof(ChattingContext))]
[Migration("20230619144448_permissionsmove")]
partial class permissionsmove
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
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<string>("DisplayName")
.HasColumnType("text");
b.Property<decimal?>("ExternalId")
.HasColumnType("numeric(20,0)");
b.Property<bool>("IsDM")
.HasColumnType("boolean");
b.Property<Guid?>("ParentChannelId")
.HasColumnType("uuid");
b.Property<int?>("PermissionsId")
.HasColumnType("integer");
b.Property<string>("Protocol")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ParentChannelId");
b.HasIndex("PermissionsId");
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<decimal?>("ExternalId")
.HasColumnType("numeric(20,0)");
b.Property<bool>("MentionsMe")
.HasColumnType("boolean");
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.PermissionSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("LewdnessFilterLevel")
.HasColumnType("integer");
b.Property<bool?>("LinksAllowed")
.HasColumnType("boolean");
b.Property<long?>("MaxAttachmentBytes")
.HasColumnType("bigint");
b.Property<long?>("MaxTextChars")
.HasColumnType("bigint");
b.Property<int?>("MeannessFilterLevel")
.HasColumnType("integer");
b.Property<bool?>("ReactionsPossible")
.HasColumnType("boolean");
b.HasKey("Id");
b.ToTable("PermissionSettings");
});
modelBuilder.Entity("vassago.Models.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal?>("ExternalId")
.HasColumnType("numeric(20,0)");
b.Property<bool>("IsBot")
.HasColumnType("boolean");
b.Property<string>("Protocol")
.HasColumnType("text");
b.Property<Guid?>("SeenInChannelId")
.HasColumnType("uuid");
b.Property<string>("Username")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("SeenInChannelId");
b.ToTable("Users");
});
modelBuilder.Entity("vassago.Models.Attachment", b =>
{
b.HasOne("vassago.Models.Message", "Message")
.WithMany("Attachments")
.HasForeignKey("MessageId");
b.Navigation("Message");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.HasOne("vassago.Models.Channel", "ParentChannel")
.WithMany("SubChannels")
.HasForeignKey("ParentChannelId");
b.HasOne("vassago.Models.PermissionSettings", "Permissions")
.WithMany()
.HasForeignKey("PermissionsId");
b.Navigation("ParentChannel");
b.Navigation("Permissions");
});
modelBuilder.Entity("vassago.Models.Message", b =>
{
b.HasOne("vassago.Models.User", "Author")
.WithMany()
.HasForeignKey("AuthorId");
b.HasOne("vassago.Models.Channel", "Channel")
.WithMany("Messages")
.HasForeignKey("ChannelId");
b.Navigation("Author");
b.Navigation("Channel");
});
modelBuilder.Entity("vassago.Models.User", b =>
{
b.HasOne("vassago.Models.Channel", "SeenInChannel")
.WithMany()
.HasForeignKey("SeenInChannelId");
b.Navigation("SeenInChannel");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.Navigation("Messages");
b.Navigation("SubChannels");
});
modelBuilder.Entity("vassago.Models.Message", b =>
{
b.Navigation("Attachments");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,70 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace vassago.Migrations
{
/// <inheritdoc />
public partial class permissionsmove : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Channels_PermissionSettings_PermissionsOverridesId",
table: "Channels");
migrationBuilder.RenameColumn(
name: "PermissionsOverridesId",
table: "Channels",
newName: "PermissionsId");
migrationBuilder.RenameIndex(
name: "IX_Channels_PermissionsOverridesId",
table: "Channels",
newName: "IX_Channels_PermissionsId");
migrationBuilder.AddColumn<bool>(
name: "ReactionsPossible",
table: "PermissionSettings",
type: "boolean",
nullable: true);
migrationBuilder.AddForeignKey(
name: "FK_Channels_PermissionSettings_PermissionsId",
table: "Channels",
column: "PermissionsId",
principalTable: "PermissionSettings",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Channels_PermissionSettings_PermissionsId",
table: "Channels");
migrationBuilder.DropColumn(
name: "ReactionsPossible",
table: "PermissionSettings");
migrationBuilder.RenameColumn(
name: "PermissionsId",
table: "Channels",
newName: "PermissionsOverridesId");
migrationBuilder.RenameIndex(
name: "IX_Channels_PermissionsId",
table: "Channels",
newName: "IX_Channels_PermissionsOverridesId");
migrationBuilder.AddForeignKey(
name: "FK_Channels_PermissionSettings_PermissionsOverridesId",
table: "Channels",
column: "PermissionsOverridesId",
principalTable: "PermissionSettings",
principalColumn: "Id");
}
}
}

View File

@ -0,0 +1,263 @@
// <auto-generated />
using System;
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
{
[DbContext(typeof(ChattingContext))]
[Migration("20230619155657_DistinguishUsersAndAccounts")]
partial class DistinguishUsersAndAccounts
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("vassago.Models.Account", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<decimal?>("ExternalId")
.HasColumnType("numeric(20,0)");
b.Property<bool>("IsBot")
.HasColumnType("boolean");
b.Property<int[]>("PermissionTags")
.HasColumnType("integer[]");
b.Property<string>("Protocol")
.HasColumnType("text");
b.Property<Guid?>("SeenInChannelId")
.HasColumnType("uuid");
b.Property<string>("Username")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("SeenInChannelId");
b.ToTable("Users");
});
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<string>("DisplayName")
.HasColumnType("text");
b.Property<decimal?>("ExternalId")
.HasColumnType("numeric(20,0)");
b.Property<bool>("IsDM")
.HasColumnType("boolean");
b.Property<Guid?>("ParentChannelId")
.HasColumnType("uuid");
b.Property<int?>("PermissionsId")
.HasColumnType("integer");
b.Property<string>("Protocol")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ParentChannelId");
b.HasIndex("PermissionsId");
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<decimal?>("ExternalId")
.HasColumnType("numeric(20,0)");
b.Property<bool>("MentionsMe")
.HasColumnType("boolean");
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.PermissionSettings", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("LewdnessFilterLevel")
.HasColumnType("integer");
b.Property<bool?>("LinksAllowed")
.HasColumnType("boolean");
b.Property<long?>("MaxAttachmentBytes")
.HasColumnType("bigint");
b.Property<long?>("MaxTextChars")
.HasColumnType("bigint");
b.Property<int?>("MeannessFilterLevel")
.HasColumnType("integer");
b.Property<bool?>("ReactionsPossible")
.HasColumnType("boolean");
b.HasKey("Id");
b.ToTable("PermissionSettings");
});
modelBuilder.Entity("vassago.Models.Account", b =>
{
b.HasOne("vassago.Models.Channel", "SeenInChannel")
.WithMany("Users")
.HasForeignKey("SeenInChannelId");
b.Navigation("SeenInChannel");
});
modelBuilder.Entity("vassago.Models.Attachment", b =>
{
b.HasOne("vassago.Models.Message", "Message")
.WithMany("Attachments")
.HasForeignKey("MessageId");
b.Navigation("Message");
});
modelBuilder.Entity("vassago.Models.Channel", b =>
{
b.HasOne("vassago.Models.Channel", "ParentChannel")
.WithMany("SubChannels")
.HasForeignKey("ParentChannelId");
b.HasOne("vassago.Models.PermissionSettings", "Permissions")
.WithMany()
.HasForeignKey("PermissionsId");
b.Navigation("ParentChannel");
b.Navigation("Permissions");
});
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");
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");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace vassago.Migrations
{
/// <inheritdoc />
public partial class DistinguishUsersAndAccounts : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "DisplayName",
table: "Users",
type: "text",
nullable: true);
migrationBuilder.AddColumn<int[]>(
name: "PermissionTags",
table: "Users",
type: "integer[]",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DisplayName",
table: "Users");
migrationBuilder.DropColumn(
name: "PermissionTags",
table: "Users");
}
}
}

View File

@ -22,6 +22,40 @@ namespace vassago.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("vassago.Models.Account", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("DisplayName")
.HasColumnType("text");
b.Property<decimal?>("ExternalId")
.HasColumnType("numeric(20,0)");
b.Property<bool>("IsBot")
.HasColumnType("boolean");
b.Property<int[]>("PermissionTags")
.HasColumnType("integer[]");
b.Property<string>("Protocol")
.HasColumnType("text");
b.Property<Guid?>("SeenInChannelId")
.HasColumnType("uuid");
b.Property<string>("Username")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("SeenInChannelId");
b.ToTable("Users");
});
modelBuilder.Entity("vassago.Models.Attachment", b => modelBuilder.Entity("vassago.Models.Attachment", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -77,7 +111,7 @@ namespace vassago.Migrations
b.Property<Guid?>("ParentChannelId") b.Property<Guid?>("ParentChannelId")
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<int?>("PermissionsOverridesId") b.Property<int?>("PermissionsId")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<string>("Protocol") b.Property<string>("Protocol")
@ -87,7 +121,7 @@ namespace vassago.Migrations
b.HasIndex("ParentChannelId"); b.HasIndex("ParentChannelId");
b.HasIndex("PermissionsOverridesId"); b.HasIndex("PermissionsId");
b.ToTable("Channels"); b.ToTable("Channels");
}); });
@ -151,37 +185,21 @@ namespace vassago.Migrations
b.Property<int?>("MeannessFilterLevel") b.Property<int?>("MeannessFilterLevel")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<bool?>("ReactionsPossible")
.HasColumnType("boolean");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("PermissionSettings"); b.ToTable("PermissionSettings");
}); });
modelBuilder.Entity("vassago.Models.User", b => modelBuilder.Entity("vassago.Models.Account", b =>
{ {
b.Property<Guid>("Id") b.HasOne("vassago.Models.Channel", "SeenInChannel")
.ValueGeneratedOnAdd() .WithMany("Users")
.HasColumnType("uuid"); .HasForeignKey("SeenInChannelId");
b.Property<decimal?>("ExternalId") b.Navigation("SeenInChannel");
.HasColumnType("numeric(20,0)");
b.Property<bool>("IsBot")
.HasColumnType("boolean");
b.Property<string>("Protocol")
.HasColumnType("text");
b.Property<Guid?>("SeenInChannelId")
.HasColumnType("uuid");
b.Property<string>("Username")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("SeenInChannelId");
b.ToTable("Users");
}); });
modelBuilder.Entity("vassago.Models.Attachment", b => modelBuilder.Entity("vassago.Models.Attachment", b =>
@ -199,18 +217,18 @@ namespace vassago.Migrations
.WithMany("SubChannels") .WithMany("SubChannels")
.HasForeignKey("ParentChannelId"); .HasForeignKey("ParentChannelId");
b.HasOne("vassago.Models.PermissionSettings", "PermissionsOverrides") b.HasOne("vassago.Models.PermissionSettings", "Permissions")
.WithMany() .WithMany()
.HasForeignKey("PermissionsOverridesId"); .HasForeignKey("PermissionsId");
b.Navigation("ParentChannel"); b.Navigation("ParentChannel");
b.Navigation("PermissionsOverrides"); b.Navigation("Permissions");
}); });
modelBuilder.Entity("vassago.Models.Message", b => modelBuilder.Entity("vassago.Models.Message", b =>
{ {
b.HasOne("vassago.Models.User", "Author") b.HasOne("vassago.Models.Account", "Author")
.WithMany() .WithMany()
.HasForeignKey("AuthorId"); .HasForeignKey("AuthorId");
@ -223,20 +241,13 @@ namespace vassago.Migrations
b.Navigation("Channel"); b.Navigation("Channel");
}); });
modelBuilder.Entity("vassago.Models.User", b =>
{
b.HasOne("vassago.Models.Channel", "SeenInChannel")
.WithMany()
.HasForeignKey("SeenInChannelId");
b.Navigation("SeenInChannel");
});
modelBuilder.Entity("vassago.Models.Channel", b => modelBuilder.Entity("vassago.Models.Channel", b =>
{ {
b.Navigation("Messages"); b.Navigation("Messages");
b.Navigation("SubChannels"); b.Navigation("SubChannels");
b.Navigation("Users");
}); });
modelBuilder.Entity("vassago.Models.Message", b => modelBuilder.Entity("vassago.Models.Message", b =>

31
Models/Account.cs Normal file
View File

@ -0,0 +1,31 @@
namespace vassago.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
public class Account
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public ulong? ExternalId { get; set; }
public string Username { get; set; }
private string _displayName = null;
public string DisplayName //TODO: fill
{
get
{
return _displayName ?? Username;
}
set
{
_displayName = value;
}
}
public bool IsBot { get; set; } //webhook counts
public Channel SeenInChannel { get; set; }
//permissions are per account-in-channel, and always propagate down. and since protocol will be a channel, I'll set the "is adam" permission on myself 1x/protocol.
public List<Enumerations.WellknownPermissions> PermissionTags{get;set;}
public string Protocol { get; set; }
}

View File

@ -14,15 +14,52 @@ public class Channel
public ulong? ExternalId { get; set; } public ulong? ExternalId { get; set; }
public string DisplayName { get; set; } public string DisplayName { get; set; }
public bool IsDM { get; set; } public bool IsDM { get; set; }
public PermissionSettings PermissionsOverrides { get; set; } public PermissionSettings Permissions { get; set; }
public List<Channel> SubChannels { get; set; } public List<Channel> SubChannels { get; set; }
public Channel ParentChannel { get; set; } public Channel ParentChannel { get; set; }
public string Protocol { get; set; } public string Protocol { get; set; }
public List<Message> Messages { get; set; } public List<Message> Messages { get; set; }
public List<Account> Users { get; set; }
//public Dictionary<string, string> EmoteOverrides{get;set;}
[NonSerialized] [NonSerialized]
public Func<string, string, Task> SendFile; public Func<string, string, Task> SendFile;
[NonSerialized] [NonSerialized]
public Func<string, Task> SendMessage; public Func<string, Task> SendMessage;
public PermissionSettings EffectivePermissions
{
get
{
PermissionSettings toReturn = Permissions ?? new PermissionSettings();
return GetEffectivePermissions(ref toReturn);
}
}
private PermissionSettings GetEffectivePermissions(ref PermissionSettings settings)
{
if(settings == null) throw new ArgumentNullException();
settings.LewdnessFilterLevel = settings.LewdnessFilterLevel ?? Permissions?.LewdnessFilterLevel;
settings.MeannessFilterLevel = settings.MeannessFilterLevel ?? Permissions?.MeannessFilterLevel;
settings.LinksAllowed = settings.LinksAllowed ?? Permissions?.LinksAllowed;
settings.MaxAttachmentBytes = settings.MaxAttachmentBytes ?? Permissions?.MaxAttachmentBytes;
settings.MaxTextChars = settings.MaxTextChars ?? Permissions?.MaxTextChars;
settings.ReactionsPossible = settings.ReactionsPossible ?? Permissions?.ReactionsPossible;
if(this.ParentChannel != null &&
(settings.LewdnessFilterLevel == null ||
settings.MeannessFilterLevel == null ||
settings.LinksAllowed == null ||
settings.MaxAttachmentBytes == null ||
settings.MaxTextChars == null ||
settings.ReactionsPossible == null))
{
return this.ParentChannel.GetEffectivePermissions(ref settings);
}
else
{
return settings;
}
}
} }

View File

@ -10,7 +10,7 @@ public class ChattingContext : DbContext
//public DbSet<Emoji> Emoji {get;set;} //public DbSet<Emoji> Emoji {get;set;}
public DbSet<Message> Messages { get; set; } public DbSet<Message> Messages { get; set; }
public DbSet<PermissionSettings> PermissionSettings{get;set;} public DbSet<PermissionSettings> PermissionSettings{get;set;}
public DbSet<User> Users { get; set; } public DbSet<Account> Users { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseNpgsql(Shared.DBConnectionString) => optionsBuilder.UseNpgsql(Shared.DBConnectionString)

View File

@ -1,33 +1,58 @@
using System;
using System.ComponentModel;
using System.Reflection;
namespace vassago.Models; namespace vassago.Models;
public static class Enumerations public static class Enumerations
{ {
public static string LewdnessFilterLevel(int level) public enum LewdnessFilterLevel
{ {
switch (level) [Description("this is a christian minecraft server 🙏")]
Strict,
[Description("G-Rated")]
G,
[Description("polite company")]
Moderate,
[Description(";) ;) ;)")]
unrestricted
}
public enum MeannessFilterLevel
{ {
case 0: [Description("good vibes only")]
return "this is a christian minecraft server 🙏"; Strict,
case 1: [Description("387.44 million miles of printed circuits, etc")]
return "G-Rated"; Unrestricted
case 2:
return "polite company";
case 3:
return ";) ;) ;)";
default:
return "ERROR";
} }
} public enum WellknownPermissions
public static string MeannessFilterLevel(int level)
{ {
switch (level) Master, //e.g., me. not that I think this would ever be released?
ChannelModerator,
}
public static string GetDescription<T>(this T enumerationValue)
where T : struct
{ {
case 0: Type type = enumerationValue.GetType();
return "good vibes only"; if (!type.IsEnum)
case 1: {
return "387.44 million miles of printed circuits, etc"; throw new ArgumentException("EnumerationValue must be of Enum type", "enumerationValue");
default: }
return "ERROR";
//Tries to find a DescriptionAttribute for a potential friendly name
//for the enum
MemberInfo[] memberInfo = type.GetMember(enumerationValue.ToString());
if (memberInfo != null && memberInfo.Length > 0)
{
object[] attrs = memberInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);
if (attrs != null && attrs.Length > 0)
{
//Pull out the description value
return ((DescriptionAttribute)attrs[0]).Description;
} }
} }
//If we have no description attribute, just return the ToString of the enum
return enumerationValue.ToString();
}
} }

View File

@ -21,7 +21,7 @@ public class Message
public DateTimeOffset Timestamp { get; set; } public DateTimeOffset Timestamp { get; set; }
public bool ActedOn { get; set; } public bool ActedOn { get; set; }
public List<Attachment> Attachments { get; set; } public List<Attachment> Attachments { get; set; }
public User Author { get; set; } public Account Author { get; set; }
public Channel Channel { get; set; } public Channel Channel { get; set; }

View File

@ -10,6 +10,7 @@ public class PermissionSettings
public uint? MaxAttachmentBytes { get; set; } public uint? MaxAttachmentBytes { get; set; }
public uint? MaxTextChars { get; set; } public uint? MaxTextChars { get; set; }
public bool? LinksAllowed { get; set; } public bool? LinksAllowed { get; set; }
public bool? ReactionsPossible { get; set; }
public int? LewdnessFilterLevel { get; set; } public int? LewdnessFilterLevel { get; set; }
public int? MeannessFilterLevel { get; set; } public int? MeannessFilterLevel { get; set; }
} }

View File

@ -5,13 +5,9 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection; using System.Reflection;
public class User //TODO: distinguish the user and their account public class User
{ {
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; } public Guid Id { get; set; }
public ulong? ExternalId { get; set; } public List<Account> Accounts { get; set; }
public string Username { get; set; } //TODO: display names. many protocols support this feature.
public bool IsBot { get; set; } //webhook counts
public Channel SeenInChannel { get; set; }
public string Protocol { get; set; }
} }