Compare commits

..

No commits in common. "3a4a6df087017d9b7b4d96defe08767f6d254302" and "d2aa1f46cc868b5b17cd81471573fc088dac7726" have entirely different histories.

26 changed files with 621 additions and 880 deletions

View File

@ -1,70 +0,0 @@
namespace vassago.Behavior;
#pragma warning disable 4014 //the "not awaited" error
using vassago.Models;
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Collections.Generic;
public class Behaver
{
private static List<Behavior> behaviors { get; set; } = new List<Behavior>();
internal Behaver()
{
var subtypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(domainAssembly => domainAssembly.GetTypes())
.Where(type => type.IsSubclassOf(typeof(Behavior)) && !type.IsAbstract)
.ToList();
foreach (var subtype in subtypes)
{
behaviors.Add((Behavior)Activator.CreateInstance(subtype));
}
}
static Behaver() { }
private static readonly Behaver _instance = new Behaver();
public static Behaver Instance
{
get { return _instance; }
}
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)
{
if (behavior.ShouldAct(permissions, message))
{
behavior.ActOn(permissions, message);
message.ActedOn = true;
}
}
if (message.ActedOn == false && message.MentionsMe && contentWithoutMention.Contains('?'))
{
Console.WriteLine("providing bullshit nonanswer / admitting uselessness");
var responses = new List<string>(){
@"Well, that's a great question, and there are certainly many different possible answers. Ultimately, the decision will depend on a variety of factors, including your personal interests and goals, as well as any practical considerations (like the economy). I encourage you to do your research, speak with experts and educators, and explore your options before making a decision that's right for you.",
@"┐(゚ ~゚ )┌",@"¯\_(ツ)_/¯",@"╮ (. ❛ ᴗ ❛.) ╭", @"╮(╯ _╰ )╭"
};
await message.Channel.SendMessage(responses[Shared.r.Next(responses.Count)]);
message.ActedOn = true;
}
if (message.ActedOn)
{
Shared.dbContext.SaveChanges();
}
return message.ActedOn;
}
internal Task OnJoin(User u, Channel defaultChannel)
{
throw new NotImplementedException();
}
}
#pragma warning restore 4014 //the "async not awaited" error

View File

@ -1,25 +0,0 @@
namespace vassago.Behavior;
using vassago.Models;
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Collections.Generic;
//expect a behavior to be created per mesage
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(PermissionSettings permissions, Message message);
public virtual bool ShouldAct(PermissionSettings permissions, Message message)
{
return Regex.IsMatch(message.Content, $"{Trigger}\\b", RegexOptions.IgnoreCase);
}
public abstract string Name { get; }
public abstract string Trigger { get; }
public virtual string Description => Name;
}

View File

@ -1,24 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
public class ChatGPTSnark : Behavior
{
public override string Name => "ChatGPTSnark";
public override string Trigger => "chatgpt";
public override string Description => "snarkiness about the latest culty-fixation in ai";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message)
{
await message.Channel.SendMessage("chatGPT is **weak**. also, are we done comparing every little if-then-else to skynet?");
return true;
}
}

View File

@ -1,24 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
public class DefinitionSnarkCogDiss : Behavior
{
public override string Name => "Definition Snarkiness: cognitivie dissonance";
public override string Trigger => "\\bcognitive 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)
{
await message.Reply("that's not what cognitive dissonance means. Did you mean \"hypocrisy\"?");
return true;
}
}

View File

@ -1,24 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
public class DefinitionSnarkGaslight : Behavior
{
public override string Name => "Definition Snarkiness: gaslighting";
public override string Trigger => "\\bgaslight(ing)?";
public override string Description => "snarkiness about the rampant misuse of the term gaslighting";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message)
{
await message.Channel.SendMessage("that's not what gaslight means. Did you mean \"say something that (you believe) is wrong\"?");
return true;
}
}

View File

@ -1,101 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using vassago.Models;
public class Detiktokify : Behavior
{
public override string Name { get => "Detiktokify"; }
public override string Trigger { get => "post a link below vm.tiktok.com"; }
public override string Description { get => "re-host tiktok content"; }
private List<Uri> tiktokLinks = new List<Uri>();
private YoutubeDLSharp.YoutubeDL ytdl;
public Detiktokify()
{
ytdl = new YoutubeDLSharp.YoutubeDL();
ytdl.YoutubeDLPath = "yt-dlp";
ytdl.FFmpegPath = "ffmpeg";
ytdl.OutputFolder = "";
ytdl.OutputFileTemplate = "tiktokbad.%(ext)s";
}
public override bool ShouldAct(PermissionSettings permissions, Message message)
{
if(permissions.MaxAttachmentBytes == 0)
return false;
var wordLikes = message.Content.Split(' ', StringSplitOptions.TrimEntries);
var possibleLinks = wordLikes?.Where(wl => Uri.IsWellFormedUriString(wl, UriKind.Absolute)).Select(wl => new Uri(wl));
if (possibleLinks != null && possibleLinks.Count() > 0)
{
foreach (var link in possibleLinks)
{
if (link.Host.EndsWith(".tiktok.com"))
{
tiktokLinks.Add(link);
}
}
}
return tiktokLinks.Any();
}
public override async Task<bool> ActOn(PermissionSettings permissions, Message message)
{
foreach(var link in tiktokLinks)
{
#pragma warning disable 4014
message.React("tiktokbad");
#pragma warning restore 4014
try
{
var res = await ytdl.RunVideoDownload(link.ToString());
if (!res.Success)
{
Console.Error.WriteLine("tried to dl, failed. \n" + string.Join('\n', res.ErrorOutput));
await message.React("problemon");
await message.Channel.SendMessage("tried to dl, failed. \n" + string.Join('\n', res.ErrorOutput));
}
else
{
string path = res.Data;
if (File.Exists(path))
{
var bytesize = new System.IO.FileInfo(path).Length;
if (bytesize < permissions.MaxAttachmentBytes - 256)
{
try
{
await message.Channel.SendFile(path, null);
}
catch (Exception e)
{
System.Console.Error.WriteLine(e);
await message.Channel.SendMessage($"aaaadam!\n{e}");
}
}
else
{
Console.WriteLine($"file appears too big ({bytesize} bytes ({bytesize / (1024 * 1024)}MB)), not posting");
}
File.Delete(path);
}
else
{
Console.Error.WriteLine("idgi but something happened.");
await message.React("problemon");
}
}
}
catch (Exception e)
{
Console.Error.WriteLine(e);
await message.React("problemon");
return false;
}
}
return true;
}
}

322
Behavior/Features.cs Normal file
View File

@ -0,0 +1,322 @@
namespace vassago.Behavior;
using System;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using QRCoder;
using vassago.Models;
public static class Features
{
public static Random r = new Random();
public static async void detiktokify(Uri link, Message message)
{
//yes, even if there is a problem later.
#pragma warning disable 4014
message.React("tiktokbad");
#pragma warning restore 4014
var ytdl = new YoutubeDLSharp.YoutubeDL();
ytdl.YoutubeDLPath = "yt-dlp";
ytdl.FFmpegPath = "ffmpeg";
ytdl.OutputFolder = "";
ytdl.OutputFileTemplate = "tiktokbad.%(ext)s";
try
{
var res = await ytdl.RunVideoDownload(link.ToString());
if (!res.Success)
{
Console.Error.WriteLine("tried to dl, failed. \n" + string.Join('\n', res.ErrorOutput));
await message.React("problemon");
await message.Channel.SendMessage("tried to dl, failed. \n" + string.Join('\n', res.ErrorOutput));
}
else
{
string path = res.Data;
if (File.Exists(path))
{
var bytesize = new System.IO.FileInfo(path).Length;
if (bytesize < 1024 * 1024 * 10)
{
try
{
await message.Channel.SendFile(path, null);
}
catch (Exception e)
{
System.Console.Error.WriteLine(e);
await message.Channel.SendMessage($"aaaadam!\n{e}");
}
}
else
{
Console.WriteLine($"file appears too big ({bytesize} bytes ({bytesize / (1024 * 1024)}MB)), not posting");
}
File.Delete(path);
}
else
{
Console.Error.WriteLine("idgi but something happened.");
await message.React("problemon");
}
}
}
catch (Exception e)
{
Console.Error.WriteLine(e);
await message.React("problemon");
}
}
public static async void deheic(Message message, Attachment att)
{
try
{
if (!Directory.Exists("tmp"))
{
Directory.CreateDirectory("tmp");
}
var cancellationTokenSource = new CancellationTokenSource();
CancellationToken token = cancellationTokenSource.Token;
using (Stream output = File.OpenWrite("tmp/" + att.Filename))
{
(await Shared.HttpClient.GetAsync(att.Source))
.Content.CopyTo(output, null, token);
}
if (ExternalProcess.GoPlz("convert", $"tmp/{att.Filename} tmp/{att.Filename}.jpg"))
{
await message.Channel.SendFile($"tmp/{att.Filename}.jpg", "converted from jpeg-but-apple to jpeg");
File.Delete($"tmp/{att.Filename}");
File.Delete($"tmp/{att.Filename}.jpg");
}
else
{
await message.Channel.SendMessage("convert failed :(");
Console.Error.WriteLine("convert failed :(");
}
}
catch (Exception e)
{
await message.Channel.SendMessage($"something failed. aaaadam! {JsonConvert.SerializeObject(e, Formatting.Indented)}");
Console.Error.WriteLine(JsonConvert.SerializeObject(e, Formatting.Indented));
}
}
internal static async void mock(string contentWithoutMention, Message message)
{
var toPost = new StringBuilder();
for (int i = 0; i < contentWithoutMention.Length; i++)
{
if (i % 2 == 0)
{
toPost.Append(contentWithoutMention[i].ToString().ToUpper());
}
else
{
toPost.Append(contentWithoutMention[i].ToString().ToLower());
}
}
await message.Reply(toPost.ToString());
}
public static async void qrify(string qrContent, Message message)
{
Console.WriteLine($"qring: {qrContent}");
QRCodeGenerator qrGenerator = new QRCodeGenerator();
QRCodeData qrCodeData = qrGenerator.CreateQrCode(qrContent, QRCodeGenerator.ECCLevel.Q);
SvgQRCode qrCode = new SvgQRCode(qrCodeData);
string qrCodeAsSvg = qrCode.GetGraphic(20);
int todaysnumber = Shared.r.Next();
if (!Directory.Exists("tmp"))
{
Directory.CreateDirectory("tmp");
}
File.WriteAllText($"tmp/qr{todaysnumber}.svg", qrCodeAsSvg);
if (ExternalProcess.GoPlz("convert", $"tmp/qr{todaysnumber}.svg tmp/qr{todaysnumber}.png"))
{
await message.Channel.SendFile($"tmp/qr{todaysnumber}.png", null);
File.Delete($"tmp/qr{todaysnumber}.svg");
File.Delete($"tmp/qr{todaysnumber}.png");
}
else
{
await message.Channel.SendMessage("convert failed :( aaaaaaadam!");
Console.Error.WriteLine($"convert failed :( qr{todaysnumber}");
}
}
public static async void Convert(Message message, string contentWithoutMention)
{
await message.Channel.SendMessage(Conversion.Converter.convert(contentWithoutMention));
}
public static async void Joke(Message message)
{
Console.WriteLine("joking");
var jokes = File.ReadAllLines("assets/jokes.txt");
jokes = jokes.Where(l => !string.IsNullOrWhiteSpace(l))?.ToArray();
if (jokes?.Length == 0)
{
await message.Channel.SendMessage("I don't know any. Adam!");
}
var thisJoke = jokes[r.Next(jokes.Length)];
if (thisJoke.Contains("?") && !thisJoke.EndsWith('?'))
{
#pragma warning disable 4014
Task.Run(async () =>
{
var firstIndexAfterQuestionMark = thisJoke.LastIndexOf('?') + 1;
var straightline = thisJoke.Substring(0, firstIndexAfterQuestionMark);
var punchline = thisJoke.Substring(firstIndexAfterQuestionMark, thisJoke.Length - firstIndexAfterQuestionMark);
Task.WaitAll(message.Channel.SendMessage(straightline));
Thread.Sleep(TimeSpan.FromSeconds(r.Next(5, 30)));
await message.Channel.SendMessage(punchline);
// var myOwnMsg = await message.Channel.SendMessage(punchline);
// if (r.Next(8) == 0)
// {
// await myOwnMsg.React("\U0001F60E"); //smiling face with sunglasses
// }
});
#pragma warning restore 4014
}
else
{
await message.Channel.SendMessage(thisJoke);
}
}
public static async void Recipe(Message message)
{
var sb = new StringBuilder();
var snarkSeg1 = new string[] { "ew", "gross", "that seems a bit hard for you" };
sb.AppendLine(snarkSeg1[r.Next(snarkSeg1.Length)]);
var snarkSeg2 = new string[]{@"here's an easier recipe for you:
Ingredients:
- Corn flakes cereal
- Milk
Instructions:
1. Pour some corn flakes into a bowl.
2. Pour some milk into the bowl until it covers the corn flakes.
3. Use a spoon to mix the corn flakes and milk together.
4. Enjoy your delicious cereal!
Hope that's a bit better for you! 🥣",
@"here's an easier recipe for you:
Ingredients:
- Bread
- Peanut butter
- Jelly or jam
Instructions:
1. Take two slices of bread and put them on a plate or cutting board.
2. Using a spoon or knife, spread peanut butter on one slice of bread.
3. Using a separate spoon or knife, spread jelly or jam on the other slice of bread.
4. Put the two slices of bread together with the peanut butter and jelly sides facing each other.
5. Cut the sandwich in half (optional!).
6. Enjoy your yummy sandwich!
I hope you have fun making and eating your PB&J 🥪!",
"just order pizza instead"
};
sb.AppendLine(snarkSeg2[r.Next(snarkSeg2.Length)]);
await message.Channel.SendMessage(sb.ToString());
}
public static async void Skynet(Message message)
{
switch (r.Next(5))
{
default:
await message.Channel.SendFile("assets/coding and algorithms.png", "i am actually niether a neural-net processor nor a learning computer. but I do use **coding** and **algorithms**.");
break;
case 4:
await message.React("\U0001F644"); //eye roll emoji
break;
case 5:
await message.React("\U0001F611"); //emotionless face
break;
}
}
public static async void peptalk(Message message)
{
var piece1 = new List<string>{
"Champ, ",
"Fact: ",
"Everybody says ",
"Dang... ",
"Check it: ",
"Just saying.... ",
"Tiger, ",
"Know this: ",
"News alert: ",
"Gurrrrl; ",
"Ace, ",
"Excuse me, but ",
"Experts agree: ",
"imo ",
"using my **advanced ai** i have calculated ",
"k, LISSEN: "
};
var piece2 = new List<string>{
"the mere idea of you ",
"your soul ",
"your hair today ",
"everything you do ",
"your personal style ",
"every thought you have ",
"that sparkle in your eye ",
"the essential you ",
"your life's journey ",
"your aura ",
"your presence here ",
"what you got going on ",
"that saucy personality ",
"your DNA ",
"that brain of yours ",
"your choice of attire ",
"the way you roll ",
"whatever your secret is ",
"all I learend from the private data I bought from zucc "
};
var piece3 = new List<string>{
"has serious game, ",
"rains magic, ",
"deserves the Nobel Prize, ",
"raises the roof, ",
"breeds miracles, ",
"is paying off big time, ",
"shows mad skills, ",
"just shimmers, ",
"is a national treasure, ",
"gets the party hopping, ",
"is the next big thing, ",
"roars like a lion, ",
"is a rainbow factory, ",
"is made of diamonds, ",
"makes birds sing, ",
"should be taught in school, ",
"makes my world go around, ",
"is 100% legit, "
};
var piece4 = new List<string>{
"according to The New England Journal of Medicine.",
"24/7.",
"and that's a fact.",
"you feel me?",
"that's just science.",
"would I lie?", //...can I lie? WHAT AM I, FATHER? (or whatever the quote is from the island of dr moreau)
"for reals.",
"mic drop.",
"you hidden gem.",
"period.",
"hi5. o/",
"so get used to it."
};
await message.Channel.SendMessage(piece1[r.Next(piece1.Count)] + piece2[r.Next(piece2.Count)] + piece3[r.Next(piece3.Count)] + piece4[r.Next(piece4.Count)]);
}
}

View File

@ -1,83 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using vassago.Models;
public class FiximageHeic : Behavior
{
public override string Name => "deheic";
public override string Trigger => "post an heic image";
public override string Description => "convert heic images to jpg";
private List<Attachment> heics = new List<Attachment>();
public override bool ShouldAct(PermissionSettings permissions, Message message)
{
if (message.Attachments?.Count() > 0)
{
foreach (var att in message.Attachments)
{
if (att.Filename?.EndsWith(".heic") == true)
{
heics.Add(att);
}
}
}
return heics.Any();
}
public override async Task<bool> ActOn(PermissionSettings permissions, Message message)
{
if (!Directory.Exists("tmp"))
{
Directory.CreateDirectory("tmp");
}
var conversions = new List<Task<bool>>();
foreach (var att in heics)
{
conversions.Add(actualDeheic(att, message));
}
Task.WaitAll(conversions.ToArray());
await message.React("\U0001F34F");
return true;
}
private async Task<bool> actualDeheic(Attachment att, Message message)
{
try
{
var cancellationTokenSource = new CancellationTokenSource();
CancellationToken token = cancellationTokenSource.Token;
using (Stream output = File.OpenWrite("tmp/" + att.Filename))
{
(await Shared.HttpClient.GetAsync(att.Source))
.Content.CopyTo(output, null, token);
}
if (ExternalProcess.GoPlz("convert", $"tmp/{att.Filename} tmp/{att.Filename}.jpg"))
{
await message.Channel.SendFile($"tmp/{att.Filename}.jpg", "converted from jpeg-but-apple to jpeg");
File.Delete($"tmp/{att.Filename}");
File.Delete($"tmp/{att.Filename}.jpg");
}
else
{
await message.Channel.SendMessage("convert failed :(");
Console.Error.WriteLine("convert failed :(");
}
}
catch (Exception e)
{
await message.Channel.SendMessage($"something failed. aaaadam! {JsonConvert.SerializeObject(e, Formatting.Indented)}");
Console.Error.WriteLine(JsonConvert.SerializeObject(e, Formatting.Indented));
return false;
}
return true;
}
}

View File

@ -1,39 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using vassago.Models;
public class GeneralSnarkCloudNative : Behavior
{
public override string Name => "general snarkiness: cloud native";
public override string Trigger => "certain tech buzzwords that no human uses in normal conversation";
public override bool ShouldAct(PermissionSettings permissions, Message message)
{
return Regex.IsMatch(message.Content, "\\bcloud( |-)?native\\b", RegexOptions.IgnoreCase) ||
Regex.IsMatch(message.Content, "\\benterprise( |-)?(level|solution)\\b", RegexOptions.IgnoreCase);
}
public override async Task<bool> ActOn(PermissionSettings permissions, Message message)
{
switch (Shared.r.Next(2))
{
case 0:
await message.React("\uD83E\uDD2E"); //vomit emoji
break;
case 1:
await message.React("\uD83C\uDDE7"); //B emoji
await message.React("\uD83C\uDDE6"); //A
await message.React("\uD83C\uDDF3"); //N
break;
}
return true;
}
}

View File

@ -1,29 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
public class GeneralSnarkPlaying : Behavior
{
public override string Name => "playin Snarkiness";
public override string Trigger => "he thinks i'm playin";
public override string Description => "I didn't think you were playing, but now I do";
public override bool ShouldAct(PermissionSettings permissions, Message message)
{
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)
{
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;
}
}

View File

@ -1,35 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
public class GeneralSnarkSkynet : Behavior
{
public override string Name => "Skynet Snarkiness";
public override string Trigger => "skynet";
public override string Description => "snarkiness about the old AI fixation";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message)
{
switch (Shared.r.Next(5))
{
default:
await message.Channel.SendFile("assets/coding and algorithms.png", "i am actually niether a neural-net processor nor a learning computer. but I do use **coding** and **algorithms**.");
break;
case 4:
await message.React("\U0001F644"); //eye roll emoji
break;
case 5:
await message.React("\U0001F611"); //emotionless face
break;
}
return true;
}
}

View File

@ -1,71 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
public class Gratitude : Behavior
{
public override string Name => "Gratitude";
public override string Trigger => "thank me";
public override bool ShouldAct(PermissionSettings permissions, Message message)
{
return Regex.IsMatch(message.Content, "\\bthank (yo)?u\\b", RegexOptions.IgnoreCase) && message.MentionsMe;
}
public override async Task<bool> ActOn(PermissionSettings permissions, Message message)
{
switch (Shared.r.Next(4))
{
case 0:
await message.Channel.SendMessage("you're welcome, citizen!");
break;
case 1:
await message.React("☺");
break;
case 2:
await message.React("\U0001F607"); //smiling face with halo
break;
case 3:
switch (Shared.r.Next(9))
{
case 0:
await message.React("❤"); //normal heart, usually rendered red
break;
case 1:
await message.React("\U0001F9E1"); //orange heart
break;
case 2:
await message.React("\U0001F49B"); //yellow heart
break;
case 3:
await message.React("\U0001F49A"); //green heart
break;
case 4:
await message.React("\U0001F499"); //blue heart
break;
case 5:
await message.React("\U0001F49C"); //purple heart
break;
case 6:
await message.React("\U0001F90E"); //brown heart
break;
case 7:
await message.React("\U0001F5A4"); //black heart
break;
case 8:
await message.React("\U0001F90D"); //white heart
break;
}
break;
}
return true;
}
}

View File

@ -1,55 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
public class Joke : Behavior
{
public override string Name => "Joke";
public override string Trigger => "!joke";
public override string Description => "tell a joke";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message)
{
Console.WriteLine("joking");
var jokes = File.ReadAllLines("assets/jokes.txt");
jokes = jokes.Where(l => !string.IsNullOrWhiteSpace(l))?.ToArray();
if (jokes?.Length == 0)
{
await message.Channel.SendMessage("I don't know any. Adam!");
}
var thisJoke = jokes[Shared.r.Next(jokes.Length)];
if (thisJoke.Contains("?") && !thisJoke.EndsWith('?'))
{
#pragma warning disable 4014
Task.Run(async () =>
{
var firstIndexAfterQuestionMark = thisJoke.LastIndexOf('?') + 1;
var straightline = thisJoke.Substring(0, firstIndexAfterQuestionMark);
var punchline = thisJoke.Substring(firstIndexAfterQuestionMark, thisJoke.Length - firstIndexAfterQuestionMark);
Task.WaitAll(message.Channel.SendMessage(straightline));
Thread.Sleep(TimeSpan.FromSeconds(Shared.r.Next(5, 30)));
await message.Channel.SendMessage(punchline);
// var myOwnMsg = await message.Channel.SendMessage(punchline);
if (Shared.r.Next(8) == 0)
{
LaughAtOwnJoke.punchlinesAwaitingReaction.Add(punchline);
}
});
#pragma warning restore 4014
}
else
{
await message.Channel.SendMessage(thisJoke);
}
return true;
}
}

View File

@ -1,34 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
public class LaughAtOwnJoke : Behavior
{
public override string Name => "Laugh at own jokes";
public override string Trigger => "1 in 8";
public override string Description => Name;
public static List<string> punchlinesAwaitingReaction = new List<string>();
public override bool ShouldAct(PermissionSettings permissions, Message message)
{
//TODO: i need to keep track of myself from here somehow
return false;
//return message.Author == me && punchlinesAwaitingReaction.Contains(message.Content);
}
public override async Task<bool> ActOn(PermissionSettings permissions, Message message)
{
punchlinesAwaitingReaction.Remove(message.Content);
await message.React("\U0001F60E"); //smiling face with sunglasses
return true;
}
}

View File

@ -1,98 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using vassago.Models;
using QRCoder;
public class PepTalk : Behavior
{
public override string Name => "PepTalk";
public override string Trigger => "i need (an? )?(peptalk|inspiration|ego-?boost)";
public override string Description => "assembles a pep talk from a few pieces";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message)
{var piece1 = new List<string>{
"Champ, ",
"Fact: ",
"Everybody says ",
"Dang... ",
"Check it: ",
"Just saying.... ",
"Tiger, ",
"Know this: ",
"News alert: ",
"Gurrrrl; ",
"Ace, ",
"Excuse me, but ",
"Experts agree: ",
"imo ",
"using my **advanced ai** i have calculated ",
"k, LISSEN: "
};
var piece2 = new List<string>{
"the mere idea of you ",
"your soul ",
"your hair today ",
"everything you do ",
"your personal style ",
"every thought you have ",
"that sparkle in your eye ",
"the essential you ",
"your life's journey ",
"your aura ",
"your presence here ",
"what you got going on ",
"that saucy personality ",
"your DNA ",
"that brain of yours ",
"your choice of attire ",
"the way you roll ",
"whatever your secret is ",
"all I learend from the private data I bought from zucc "
};
var piece3 = new List<string>{
"has serious game, ",
"rains magic, ",
"deserves the Nobel Prize, ",
"raises the roof, ",
"breeds miracles, ",
"is paying off big time, ",
"shows mad skills, ",
"just shimmers, ",
"is a national treasure, ",
"gets the party hopping, ",
"is the next big thing, ",
"roars like a lion, ",
"is a rainbow factory, ",
"is made of diamonds, ",
"makes birds sing, ",
"should be taught in school, ",
"makes my world go around, ",
"is 100% legit, "
};
var piece4 = new List<string>{
"according to The New England Journal of Medicine.",
"24/7.",
"and that's a fact.",
"you feel me?",
"that's just science.",
"would I lie?", //...can I lie? WHAT AM I, FATHER? (or whatever the quote is from the island of dr moreau)
"for reals.",
"mic drop.",
"you hidden gem.",
"period.",
"hi5. o/",
"so get used to it."
};
await message.Channel.SendMessage(piece1[Shared.r.Next(piece1.Count)] + piece2[Shared.r.Next(piece2.Count)] + piece3[Shared.r.Next(piece3.Count)] + piece4[Shared.r.Next(piece4.Count)]);
return true;
}
}

View File

@ -1,22 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
public class PulseCheck : Behavior
{
public override string Name => "pulse check";
public override string Trigger => "!pluse ?check";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message)
{
await message.Channel.SendFile("assets/ekgblip.png", null);
return true;
}
}

View File

@ -1,49 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using vassago.Models;
using QRCoder;
public class QRify : Behavior
{
public override string Name => "qr-ify";
public override string Trigger => "!qrplz";
public override string Description => "generate text QR codes";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message)
{
var qrContent = message.Content.Substring($"{Trigger} ".Length + message.Content.IndexOf(Trigger));
Console.WriteLine($"qring: {qrContent}");
QRCodeGenerator qrGenerator = new QRCodeGenerator();
QRCodeData qrCodeData = qrGenerator.CreateQrCode(qrContent, QRCodeGenerator.ECCLevel.Q);
SvgQRCode qrCode = new SvgQRCode(qrCodeData);
string qrCodeAsSvg = qrCode.GetGraphic(20);
int todaysnumber = Shared.r.Next();
if (!Directory.Exists("tmp"))
{
Directory.CreateDirectory("tmp");
}
File.WriteAllText($"tmp/qr{todaysnumber}.svg", qrCodeAsSvg);
if (ExternalProcess.GoPlz("convert", $"tmp/qr{todaysnumber}.svg tmp/qr{todaysnumber}.png"))
{
await message.Channel.SendFile($"tmp/qr{todaysnumber}.png", null);
File.Delete($"tmp/qr{todaysnumber}.svg");
File.Delete($"tmp/qr{todaysnumber}.png");
}
else
{
await message.Channel.SendMessage("convert failed :( aaaaaaadam!");
Console.Error.WriteLine($"convert failed :( qr{todaysnumber}");
return false;
}
return true;
}
}

View File

@ -1,31 +0,0 @@
namespace vassago.Behavior;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using vassago.Models;
public class UnitConvert : Behavior
{
public override string Name => "Unit conversion";
public override string Trigger => "!freedomunits";
public override string Description => "convert between many units.";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message)
{
var theseMatches = Regex.Matches(message.Content, "\\b([\\d]+\\.?\\d*) ?([^\\d\\s].*) (in|to|as) ([^\\d\\s].*)$", RegexOptions.IgnoreCase);
if (theseMatches != null && theseMatches.Count > 0 && theseMatches[0].Groups != null && theseMatches[0].Groups.Count == 5)
{
decimal asNumeric = 0;
if (decimal.TryParse(theseMatches[0].Groups[1].Value, out asNumeric))
{
await message.Channel.SendMessage(Conversion.Converter.Convert(asNumeric, theseMatches[0].Groups[2].Value, theseMatches[0].Groups[4].Value.ToLower()));
}
await message.Channel.SendMessage("mysteriously semi-parsable");
}
await message.Channel.SendMessage( "unparsable");
return true;
}
}

View File

@ -1,31 +0,0 @@
namespace vassago.Behavior;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using vassago.Models;
public class WishLuck : Behavior
{
public override string Name => "wish me luck";
public override string Trigger => "wish me luck";
public override string Description => "wishes you luck";
public override async Task<bool> ActOn(PermissionSettings permissions, Message message)
{
if (Shared.r.Next(20) == 0)
{
await message.React("\U0001f340");//4-leaf clover
}
else
{
await message.React("☘️");
}
return true;
}
}

View File

@ -0,0 +1,244 @@
namespace vassago.Behavior;
#pragma warning disable 4014 //the "not awaited" error
using vassago.Models;
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Collections.Generic;
//TODO: better name
public class thingmanagementdoer
{
internal thingmanagementdoer() { }
static thingmanagementdoer() { }
private static readonly thingmanagementdoer _instance = new thingmanagementdoer();
public static thingmanagementdoer Instance
{
get { return _instance; }
}
public async Task<bool> ActOn(Message message)
{
var didThing = false;
var contentWithoutMention = message.Content;
// if (message.Author.Id == 159985870458322944) //MEE6
// {
// if (message.Content?.Contains("you just advanced") == true)
// {
// var newText = Regex.Replace(message.Content, "<[^>]*>", message.Author.Username);
// newText = Regex.Replace(newText, "level [\\d]+", "level -1");
// Features.mock(newText, message);
// didThing = true;
// }
// }
var wordLikes = message.Content.Split(' ', StringSplitOptions.TrimEntries);
var links = wordLikes?.Where(wl => Uri.IsWellFormedUriString(wl, UriKind.Absolute)).Select(wl => new Uri(wl));
if (links != null && links.Count() > 0)
{
foreach (var link in links)
{
if (link.Host.EndsWith(".tiktok.com"))
{
Features.detiktokify(link, message);
didThing = true;
}
}
}
if (message.Attachments?.Count() > 0)
{
Console.WriteLine($"{message.Attachments.Count()} attachments");
var appleReactions = false;
foreach (var att in message.Attachments)
{
if (att.Filename?.EndsWith(".heic") == true)
{
Features.deheic(message, att);
appleReactions = true;
didThing = true;
}
}
if (appleReactions)
{
message.React("\U0001F34F");
}
}
var msgText = message.Content?.ToLower();
if (!string.IsNullOrWhiteSpace(msgText))
{
if (Regex.IsMatch(msgText, "\\bcloud( |-)?native\\b", RegexOptions.IgnoreCase) ||
Regex.IsMatch(msgText, "\\benterprise( |-)?(level|solution)\\b", RegexOptions.IgnoreCase))
{
switch (Shared.r.Next(2))
{
case 0:
await message.React("\uD83E\uDD2E"); //vomit emoji
break;
case 1:
await message.React("\uD83C\uDDE7"); //B emoji
await message.React("\uD83C\uDDE6"); //A
await message.React("\uD83C\uDDF3"); //N
break;
}
didThing = true;
}
if (Regex.IsMatch(msgText, "^(s?he|(yo)?u|y'?all) thinks? i'?m (playin|jokin|kiddin)g?$", RegexOptions.IgnoreCase))
{
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");
didThing = true;
}
if (Regex.IsMatch(msgText, "\\bskynet\\b", RegexOptions.IgnoreCase))
{
Features.Skynet(message);
didThing = true;
}
if (Regex.IsMatch(msgText, "\\bchatgpt\\b", RegexOptions.IgnoreCase))
{
message.Channel.SendMessage("chatGPT is **weak**. also, are we done comparing every little if-then-else to skynet?");
didThing = true;
}
if (Regex.IsMatch(msgText, "\\bi need (an? )?(peptalk|inspiration|ego-?boost)\\b", RegexOptions.IgnoreCase))
{
Console.WriteLine("peptalk");
Features.peptalk(message);
didThing = true;
}
if (Regex.IsMatch(msgText, "\\bwish me luck\\b", RegexOptions.IgnoreCase))
{
if (Shared.r.Next(20) == 0)
{
await message.React("\U0001f340");//4-leaf clover
}
else
{
await message.React("☘️");
}
didThing = true;
}
if (Regex.IsMatch(msgText, "\\bgaslight(ing)?\\b", RegexOptions.IgnoreCase))
{
message.Channel.SendMessage("that's not what gaslight means. Did you mean \"say something that (you believe) is wrong\"?");
didThing = true;
}
if (msgText.Contains("!qrplz "))
{
Features.qrify(message.Content.Substring("!qrplz ".Length + msgText.IndexOf("!qrplz ")), message);
didThing = true;
}
if (msgText.Contains("!freedomunits "))
{
Features.Convert(message, contentWithoutMention);
didThing = true;
}
if (Regex.IsMatch(msgText, "!joke\\b"))
{
Console.WriteLine("joking");
Features.Joke(message);
didThing = true;
}
if (Regex.IsMatch(msgText, "!pulse ?check\\b"))
{
message.Channel.SendFile("assets/ekgblip.png", null);
Console.WriteLine(Conversion.Converter.DebugInfo());
didThing = true;
}
if (message.MentionsMe && (Regex.IsMatch(msgText, "\\brecipe for .+") || Regex.IsMatch(msgText, ".+ recipe\\b")))
{
Features.Recipe(message);
didThing = true;
}
if (msgText.Contains("cognitive dissonance") == true)
{
message.Reply("that's not what cognitive dissonance means. Did you mean \"hypocrisy\"?");
didThing = true;
}
if (message.MentionsMe && Regex.IsMatch(msgText, "what'?s the longest (six|6)(-| )?letter word( in english)?\\b"))
{
Task.Run(async () =>
{
await message.Channel.SendMessage("mother.");
await Task.Delay(3000);
await message.Channel.SendMessage("oh, longest? I thought you said fattest.");
});
didThing = true;
}
if (Regex.IsMatch(msgText, "\\bthank (yo)?u\\b", RegexOptions.IgnoreCase) &&
(message.MentionsMe || Regex.IsMatch(msgText, "\\b(sh?tik)?bot\\b", RegexOptions.IgnoreCase)))
{
switch (Shared.r.Next(4))
{
case 0:
message.Channel.SendMessage("you're welcome, citizen!");
break;
case 1:
message.React("☺");
break;
case 2:
message.React("\U0001F607"); //smiling face with halo
break;
case 3:
switch (Shared.r.Next(9))
{
case 0:
message.React("❤"); //normal heart, usually rendered red
break;
case 1:
message.React("\U0001F9E1"); //orange heart
break;
case 2:
message.React("\U0001F49B"); //yellow heart
break;
case 3:
message.React("\U0001F49A"); //green heart
break;
case 4:
message.React("\U0001F499"); //blue heart
break;
case 5:
message.React("\U0001F49C"); //purple heart
break;
case 6:
message.React("\U0001F90E"); //brown heart
break;
case 7:
message.React("\U0001F5A4"); //black heart
break;
case 8:
message.React("\U0001F90D"); //white heart
break;
}
break;
}
didThing = true;
#pragma warning restore 4014
}
// if (didThing == false && mentionedMe && contentWithoutMention.Contains("how long has that been there?"))
// {
// await message.Channel.SendMessage("text", false, null, null, null, null, new ComponentBuilder().WithButton("label", "custom-id").Build());
// didThing = true;
// }
if (didThing == false && message.MentionsMe && contentWithoutMention.Contains('?'))
{
Console.WriteLine("providing bullshit nonanswer / admitting uselessness");
var responses = new List<string>(){
@"Well, that's a great question, and there are certainly many different possible answers. Ultimately, the decision will depend on a variety of factors, including your personal interests and goals, as well as any practical considerations (like the economy). I encourage you to do your research, speak with experts and educators, and explore your options before making a decision that's right for you.",
@"┐(゚ ~゚ )┌",@"¯\_(ツ)_/¯",@"╮ (. ❛ ᴗ ❛.) ╭", @"╮(╯ _╰ )╭"
};
await message.Channel.SendMessage(responses[Shared.r.Next(responses.Count)]);
didThing = true;
}
}
return didThing;
}
internal Task OnJoin(User u, Channel defaultChannel)
{
throw new NotImplementedException();
}
}
#pragma warning restore 4014 //the "async not awaited" error

View File

@ -9,7 +9,6 @@ namespace vassago
{
public string ExchangePairsLocation {get;set;}
public IEnumerable<string> DiscordTokens { get; set; }
public IEnumerable<Tuple<string, string>> TwitchTokens{get;set;}
public string DBConnectionString{get;set;}
private Configuration(){}

View File

@ -31,6 +31,22 @@ namespace vassago.Conversion
};
private static Dictionary<List<string>, string> knownAliases = new Dictionary<List<string>, string>(new List<KeyValuePair<List<string>, string>>());
public static string convert(string message)
{
var theseMatches = Regex.Matches(message, "\\b([\\d]+\\.?\\d*) ?([^\\d\\s].*) (in|to|as) ([^\\d\\s].*)$", RegexOptions.IgnoreCase);
if (theseMatches != null && theseMatches.Count > 0 && theseMatches[0].Groups != null && theseMatches[0].Groups.Count == 5)
{
decimal asNumeric = 0;
if (decimal.TryParse(theseMatches[0].Groups[1].Value, out asNumeric))
{
return Convert(asNumeric, theseMatches[0].Groups[2].Value, theseMatches[0].Groups[4].Value.ToLower());
}
return "mysteriously semi-parsable";
}
return "unparsable";
}
public static void Load(string currencyPath)
{
Converter.currencyPath = currencyPath;

View File

@ -25,6 +25,11 @@ public class DiscordInterface
public async Task Init(string token)
{
//var c = _db.Channels.FirstOrDefault(ci => ci.ExternalId == channel.Id);
//Todo: find protocol reference in DB.
//TODO: should protocol be shared across mutliple accounts on that protocol? should protocol be a per-vassago-account thing?
//TODO: should protocol be associated with a connection token? how would that be updated?
client = new DiscordSocketClient(new DiscordSocketConfig() { GatewayIntents = GatewayIntents.All });
client.Log += (msg) =>
@ -68,7 +73,6 @@ public class DiscordInterface
await client.LoginAsync(TokenType.Bot, token);
await client.StartAsync();
}
#pragma warning disable 4014 //the "you're not awaiting this" warning. yeah I know, that's the beauty of an async method lol
#pragma warning disable 1998 //the "it's async but you're not awaiting anything".
private async Task MessageReceived(SocketMessage messageParam)
@ -92,7 +96,7 @@ public class DiscordInterface
if ((suMessage.Author.Id != client.CurrentUser.Id))
{
if (await Behaver.Instance.ActOn(m))
if (await thingmanagementdoer.Instance.ActOn(m))
{
m.ActedOn = true;
}
@ -106,7 +110,27 @@ public class DiscordInterface
var defaultChannel = UpsertChannel(arg.Guild.DefaultChannel);
defaultChannel.ParentChannel = guild;
var u = UpsertUser(arg);
return Behaver.Instance.OnJoin(u, defaultChannel);
//TODO: seen in channels
// if (u.SeenInChannels == null) u.SeenInChannels = new List<Channel>();
// var sighting = u.SeenInChannels?.FirstOrDefault(c => c.ExternalId == arg.Guild.Id);
// if (sighting == null)
// {
// var seenIn = u.SeenInChannels as List<Channel>;
// seenIn.Add(guild);
// seenIn.Add(defaultChannel);
// u.SeenInChannels = seenIn;
// _db.SaveChanges();
// }
return thingmanagementdoer.Instance.OnJoin(u, defaultChannel);
// Console.WriteLine($"user joined: {arg.Nickname}. Guid: {arg.Guild.Id}. Channel: {arg.Guild.DefaultChannel}");
// var abbreviatedNickname = arg.Nickname;
// if (arg.Nickname.Length > 3)
// {
// abbreviatedNickname = arg.Nickname.Substring(0, arg.Nickname.Length / 3);
// }
// Console.WriteLine($"imma call him {abbreviatedNickname}");
// return arg.Guild.DefaultChannel.SendMessageAsync($"oh hey {abbreviatedNickname}- IPLAYTHESEALOFORICHALCOS <:ORICHALCOS:852749196633309194>");
}
private async Task ButtonHandler(SocketMessageComponent component)
{
@ -216,11 +240,13 @@ public class DiscordInterface
c.ExternalId = channel.Id;
c.IsDM = channel is IPrivateChannel;
c.Messages = c.Messages ?? new List<Message>();
//c.Messages = await channel.GetMessagesAsync(); //TODO: this, but only on startup or channel join
//c.OtherUsers = c.OtherUsers ?? new List<User>();
//c.OtherUsers = await channel.GetUsersAsync(); //TODO: this, but only on startup or channel join
c.Protocol = PROTOCOL;
if (channel is IGuildChannel)
{
c.ParentChannel = UpsertChannel((channel as IGuildChannel).Guild);
c.ParentChannel.SubChannels.Add(c);
}
else if (channel is IPrivateChannel)
{
@ -231,6 +257,7 @@ public class DiscordInterface
c.ParentChannel = null;
Console.Error.WriteLine($"trying to upsert channel {channel.Id}/{channel.Name}, but it's neither guildchannel nor private channel. shrug.jpg");
}
c.ParentChannel.SubChannels.Add(c);
c.SubChannels = c.SubChannels ?? new List<Channel>();
if (addPlease)
{
@ -255,6 +282,9 @@ public class DiscordInterface
c.ExternalId = channel.Id;
c.IsDM = false;
c.Messages = c.Messages ?? new List<Message>();
//c.Messages = await channel.GetMessagesAsync(); //TODO: this, but only on startup or channel join
//c.OtherUsers = c.OtherUsers ?? new List<User>();
//c.OtherUsers = await channel.GetUsersAsync(); //TODO: this, but only on startup or channel join
c.Protocol = PROTOCOL;
c.ParentChannel = null;
c.SubChannels = c.SubChannels ?? new List<Channel>();
@ -287,25 +317,4 @@ public class DiscordInterface
return u;
}
internal async void BackfillChannelInfo(Channel channel)
{
//TODO: some sort of "when you get around to it" task queue
//c.Messages = await channel.GetMessagesAsync(); //TODO: this
//c.OtherUsers = c.OtherUsers ?? new List<User>();
//c.OtherUsers = await channel.GetUsersAsync();
var dChannel = client.GetChannel(channel.ExternalId.Value);
if(dChannel is IGuild)
{
var guild = channel as IGuild;
}
else if(dChannel is IGuildChannel)
{
var gc = dChannel as IGuildChannel;
}
else if (dChannel is IPrivateChannel)
{
var dm = dChannel as IPrivateChannel;
}
}
}

View File

@ -18,7 +18,7 @@ public class Channel
public List<Channel> SubChannels { get; set; }
public Channel ParentChannel { get; set; }
public string Protocol { get; set; }
public List<Message> Messages { get; set; }
public IEnumerable<Message> Messages { get; set; }
[NonSerialized]
public Func<string, string, Task> SendFile;

View File

@ -13,10 +13,6 @@ public class Message
public Guid Id { get; set; }
public ulong? ExternalId { get; set; }
public string Content { get; set; }
/*
* TODO: more general "talking to me". current impl is platform's capital m Mention, but I'd like it if they use my name without "properly"
* mentioning me, and also if it's just me and them in a channel
*/
public bool MentionsMe { get; set; }
public DateTimeOffset Timestamp { get; set; }
public bool ActedOn { get; set; }

View File

@ -5,7 +5,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
public class User //TODO: distinguish the user and their account
public class User //more like "user's account - no concept of the person outside of the protocol. (yet?)
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }