diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..34c3e19 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "7.0.5", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/Behavior/Features.cs b/Behavior/Features.cs new file mode 100644 index 0000000..e082ba7 --- /dev/null +++ b/Behavior/Features.cs @@ -0,0 +1,320 @@ +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); + } + 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"); + 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) + { + 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))); + 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{ + "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{ + "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{ + "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{ + "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)]); + } +} diff --git a/Behavior/thingmanagementdoer.cs b/Behavior/thingmanagementdoer.cs new file mode 100644 index 0000000..8b79e50 --- /dev/null +++ b/Behavior/thingmanagementdoer.cs @@ -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 ActOn(Message message) + { + var didThing = false; + var contentWithoutMention = message.Content; + var mentionedMe = false; + // 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")) + { + Features.Joke(message); + didThing = true; + } + if (Regex.IsMatch(msgText, "!pulse ?check\\b")) + { + message.Channel.SendFile("assets/ekgblip.png"); + Console.WriteLine(Conversion.Converter.DebugInfo()); + didThing = true; + } + if (mentionedMe && (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 (mentionedMe && 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) && + (mentionedMe || 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 && mentionedMe && contentWithoutMention.Contains('?')) + { + Console.WriteLine("providing bullshit nonanswer / admitting uselessness"); + var responses = new List(){ + @"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 \ No newline at end of file diff --git a/Configuration.cs b/Configuration.cs index d41b49c..5e77965 100644 --- a/Configuration.cs +++ b/Configuration.cs @@ -9,6 +9,8 @@ namespace vassago { public string ExchangePairsLocation {get;set;} public IEnumerable DiscordTokens { get; set; } + public string DBConnectionString{get;set;} + private Configuration(){} public static Configuration Parse(string configurationPath) { diff --git a/Discord/DiscordInterface.cs b/Discord/DiscordInterface.cs deleted file mode 100644 index 891e2b1..0000000 --- a/Discord/DiscordInterface.cs +++ /dev/null @@ -1,330 +0,0 @@ -//https://discord.com/oauth2/authorize?client_id=913003037348491264&permissions=274877942784&scope=bot%20messages.read -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Discord; -using Discord.WebSocket; - -namespace vassago.Discord_Vassago; - -public class DiscordInterface -{ - private DiscordSocketClient _client; - private bool eventsSignedUp = false; - - public async Task Init(string token) - { - _client = new DiscordSocketClient(new DiscordSocketConfig() { GatewayIntents = GatewayIntents.All }); - - _client.Log += (msg) => { - Console.WriteLine(msg.ToString()); - return Task.CompletedTask; - }; - - _client.Ready += () => Task.Run(() => - { - if (!eventsSignedUp) - { - eventsSignedUp = true; - Console.WriteLine("Bot is connected! going to sign up for message received and user joined in client ready"); - - _client.MessageReceived += MessageReceived; - _client.UserJoined += UserJoined; - //_client.ButtonExecuted += MyButtonHandler; - _client.SlashCommandExecuted += SlashCommandHandler; - SlashCommandsHelper.Register(_client).GetAwaiter().GetResult(); - } - else - { - Console.WriteLine("bot appears to be RE connected, so I'm not going to sign up twice"); - } - }); - - 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) -#pragma warning restore 1998 - { - var message = messageParam as SocketUserMessage; - if (message == null) return; - if (message.Author.Id == _client.CurrentUser.Id) return; - - Console.WriteLine($"#{message.Channel}[{DateTime.Now}][{message.Author.Username} [id={message.Author.Id}]][msg id: {message.Id}] {message.Content}"); - - if (message.Author.IsWebhook || message.Author.IsBot) - { - 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); - } - } - } - else - { - var didThing = false; - var contentWithoutMention = message.Content; - var mentionedMe = false; - if (message.MentionedUsers?.FirstOrDefault(muid => muid.Id == _client.CurrentUser.Id) != null) - { - var mentionOfMe = "<@" + _client.CurrentUser.Id + ">"; - contentWithoutMention = message.Content.Replace(mentionOfMe + " ", null); - contentWithoutMention = contentWithoutMention.Replace(mentionOfMe, null); - mentionedMe = 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.AddReactionAsync(new Emoji("\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.AddReactionAsync(new Emoji("\uD83E\uDD2E")); //vomit emoji - break; - case 1: - await message.AddReactionAsync(new Emoji("\uD83C\uDDE7")); //B emoji - await message.AddReactionAsync(new Emoji("\uD83C\uDDE6")); //A - await message.AddReactionAsync(new Emoji("\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.SendMessageAsync("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.SendMessageAsync("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.AddReactionAsync(new Emoji("\U0001f340"));//4-leaf clover - } - else - { - await message.AddReactionAsync(new Emoji("☘️")); - } - didThing = true; - } - if (Regex.IsMatch(msgText, "\\bgaslight(ing)?\\b", RegexOptions.IgnoreCase)) - { - message.Channel.SendMessageAsync("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")) - { - Features.Joke(message); - didThing = true; - } - if (Regex.IsMatch(msgText, "!pulse ?check\\b")) - { - message.Channel.SendFileAsync("assets/ekgblip.png"); - Console.WriteLine(Conversion.Converter.DebugInfo()); - didThing = true; - } - if (mentionedMe && (Regex.IsMatch(msgText, "\\brecipe for .+") || Regex.IsMatch(msgText, ".+ recipe\\b"))) - { - Features.Recipe(message); - didThing = true; - } - if (msgText.Contains("cognitive dissonance") == true) - { - message.ReplyAsync("that's not what cognitive dissonance means. Did you mean \"hypocrisy\"?"); - didThing = true; - } - if (mentionedMe && Regex.IsMatch(msgText, "what'?s the longest (six|6)(-| )?letter word( in english)?\\b")) - { - Task.Run(async () => - { - await message.Channel.SendMessageAsync("mother."); - await Task.Delay(3000); - await message.Channel.SendMessageAsync("oh, longest? I thought you said fattest."); - }); - didThing = true; - } - if (Regex.IsMatch(msgText, "\\bthank (yo)?u\\b", RegexOptions.IgnoreCase) && - (mentionedMe || Regex.IsMatch(msgText, "\\b(sh?tik)?bot\\b", RegexOptions.IgnoreCase))) - { - switch (Shared.r.Next(4)) - { - case 0: - message.Channel.SendMessageAsync("you're welcome, citizen!"); - break; - case 1: - message.AddReactionAsync(new Emoji("☺")); - break; - case 2: - message.AddReactionAsync(new Emoji("\U0001F607")); //smiling face with halo - break; - case 3: - switch (Shared.r.Next(9)) - { - case 0: - message.AddReactionAsync(new Emoji("❤")); //normal heart, usually rendered red - break; - case 1: - message.AddReactionAsync(new Emoji("\U0001F9E1")); //orange heart - break; - case 2: - message.AddReactionAsync(new Emoji("\U0001F49B")); //yellow heart - break; - case 3: - message.AddReactionAsync(new Emoji("\U0001F49A")); //green heart - break; - case 4: - message.AddReactionAsync(new Emoji("\U0001F499")); //blue heart - break; - case 5: - message.AddReactionAsync(new Emoji("\U0001F49C")); //purple heart - break; - case 6: - message.AddReactionAsync(new Emoji("\U0001F90E")); //brown heart - break; - case 7: - message.AddReactionAsync(new Emoji("\U0001F5A4")); //black heart - break; - case 8: - message.AddReactionAsync(new Emoji("\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.SendMessageAsync("text", false, null, null, null, null, new ComponentBuilder().WithButton("label", "custom-id").Build()); - // didThing = true; - // } - if (didThing == false && mentionedMe && contentWithoutMention.Contains('?')) - { - Console.WriteLine("providing bullshit nonanswer / admitting uselessness"); - var responses = new List(){ - @"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.SendMessageAsync(responses[Shared.r.Next(responses.Count)]); - didThing = true; - } - } - } - } - private Task UserJoined(SocketGuildUser arg) - { - 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) - { - switch (component.Data.CustomId) - { - case "custom-id": - await component.RespondAsync($"{component.User.Mention}, it's been here the whole time!"); - break; - } - } - internal static async Task SlashCommandHandler(SocketSlashCommand command) - { - switch (command.CommandName) - { - case "freedomunits": - try - { - var amt = Convert.ToDecimal((double)(command.Data.Options.First(o => o.Name == "amount").Value)); - var src = (string)command.Data.Options.First(o => o.Name == "src-unit").Value; - var dest = (string)command.Data.Options.First(o => o.Name == "dest-unit").Value; - var conversionResult = Conversion.Converter.Convert(amt, src, dest); - - await command.RespondAsync($"> {amt} {src} -> {dest}\n{conversionResult}"); - } - catch (Exception e) - { - await command.RespondAsync($"error: {e.Message}. aaadam!"); - } - break; - default: - await command.RespondAsync($"\\*smiles and nods*\n"); - await command.Channel.SendFileAsync($"assets/loud sweating.gif"); - Console.Error.WriteLine($"can't understand command name: {command.CommandName}"); - break; - } - } - -} \ No newline at end of file diff --git a/DiscordInterface/DiscordInterface.cs b/DiscordInterface/DiscordInterface.cs new file mode 100644 index 0000000..1b304df --- /dev/null +++ b/DiscordInterface/DiscordInterface.cs @@ -0,0 +1,216 @@ +//https://discord.com/oauth2/authorize?client_id=913003037348491264&permissions=274877942784&scope=bot%20messages.read +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; + using vassago.Models; +using vassago.DiscordInterface.Models; +using vassago.Behavior; + +namespace vassago.DiscordInterface; + +public class DiscordInterface +{ + private DiscordSocketClient _client; + private DiscordProtocol protocolInterface; + private bool eventsSignedUp = false; + private ChattingContext _db; + public DiscordInterface() + { + _db = Shared.dbContext; + } + + public async Task Init(string token) + { + _client = new DiscordSocketClient(new DiscordSocketConfig() { GatewayIntents = GatewayIntents.All }); + + _client.Log += (msg) => + { + Console.WriteLine(msg.ToString()); + return Task.CompletedTask; + }; + + _client.Ready += () => Task.Run(() => + { + if (!eventsSignedUp) + { + eventsSignedUp = true; + Console.WriteLine("Bot is connected! going to sign up for message received and user joined in client ready"); + + _client.MessageReceived += MessageReceived; + // _client.MessageUpdated += + _client.UserJoined += UserJoined; + _client.SlashCommandExecuted += SlashCommandHandler; + // _client.ChannelCreated += + // _client.ChannelDestroyed += + // _client.ChannelUpdated += + // _client.GuildMemberUpdated += + // _client.UserBanned += + // _client.UserLeft += + // _client.ThreadCreated += + // _client.ThreadUpdated += + // _client.ThreadDeleted += + // _client.JoinedGuild += + // _client.GuildUpdated += + // _client.LeftGuild += + + SlashCommandsHelper.Register(_client).GetAwaiter().GetResult(); + } + else + { + Console.WriteLine("bot appears to be RE connected, so I'm not going to sign up twice"); + } + }); + + 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) +#pragma warning restore 1998 + { + var suMessage = messageParam as SocketUserMessage; + if (suMessage == null) return; + + var m = _db.Messages.FirstOrDefault(mi => mi.ExternalId == suMessage.Id) as DiscordMessage; + if(m == null) + { + m = _db.Messages.Add(new DiscordMessage(suMessage)).Entity as DiscordMessage; + } + m.Intake(suMessage, _client.CurrentUser.Id); + + m.Channel = UpsertChannel(suMessage.Channel); + m.Author = UpsertUser(suMessage.Author); + _db.SaveChanges(); + Console.WriteLine($"#{suMessage.Channel}[{DateTime.Now}][{suMessage.Author.Username} [id={suMessage.Author.Id}]][msg id: {suMessage.Id}] {suMessage.Content}"); + if (suMessage.Author.Id == _client.CurrentUser.Id) return; + + + if (suMessage.MentionedUsers?.FirstOrDefault(muid => muid.Id == _client.CurrentUser.Id) != null) + { + var mentionOfMe = "<@" + _client.CurrentUser.Id + ">"; + m.MentionsMe = true; + } + + //TODO: standardize content + + if(await thingmanagementdoer.Instance.ActOn(m)) + { + m.ActedOn = true; + _db.SaveChanges(); + } + } + + private Task UserJoined(SocketGuildUser arg) + { + var guild = UpsertChannel(arg.Guild); + var defaultChannel = UpsertChannel(arg.Guild.DefaultChannel); + defaultChannel.ParentChannel = guild; + var u = UpsertUser(arg); + if(u.SeenInChannels == null) u.SeenInChannels = new List(); + var sighting = u.SeenInChannels?.FirstOrDefault(c => c.ExternalId == arg.Guild.Id); + if(sighting == null) + { + var seenIn = u.SeenInChannels as List; + 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) + { + switch (component.Data.CustomId) + { + case "custom-id": + await component.RespondAsync($"{component.User.Mention}, it's been here the whole time!"); + break; + } + } + internal static async Task SlashCommandHandler(SocketSlashCommand command) + { + switch (command.CommandName) + { + case "freedomunits": + try + { + var amt = Convert.ToDecimal((double)(command.Data.Options.First(o => o.Name == "amount").Value)); + var src = (string)command.Data.Options.First(o => o.Name == "src-unit").Value; + var dest = (string)command.Data.Options.First(o => o.Name == "dest-unit").Value; + var conversionResult = Conversion.Converter.Convert(amt, src, dest); + + await command.RespondAsync($"> {amt} {src} -> {dest}\n{conversionResult}"); + } + catch (Exception e) + { + await command.RespondAsync($"error: {e.Message}. aaadam!"); + } + break; + default: + await command.RespondAsync($"\\*smiles and nods*\n"); + await command.Channel.SendFileAsync($"assets/loud sweating.gif"); + Console.Error.WriteLine($"can't understand command name: {command.CommandName}"); + break; + } + } + + private Channel UpsertChannel(ISocketMessageChannel channel) + { + var c = _db.Channels.FirstOrDefault(ci => ci.ExternalId == channel.Id); + if(c == null) + { + c = _db.Channels.Add(new DiscordChannel()).Entity; + _db.SaveChanges(); + } + if(channel is IGuildChannel) + { + c.ParentChannel = UpsertChannel((channel as IGuildChannel).Guild); + } + else if (channel is IPrivateChannel) + { + c.ParentChannel = protocolInterface; + } + else + { + c.ParentChannel = protocolInterface; + Console.WriteLine($"trying to upsert channel {channel.Id}/{channel.Name}, but it's neither guildchannel nor private channel. shrug.jpg"); + } + return c; + } + private Channel UpsertChannel(IGuild channel) + { + var c = _db.Channels.FirstOrDefault(ci => ci.ExternalId == channel.Id); + if(c == null) + { + c = _db.Channels.Add(new DiscordChannel()).Entity; + _db.SaveChanges(); + } + c.ParentChannel = protocolInterface; + return c; + } + + private User UpsertUser(SocketUser user) + { + var u = _db.Users.FirstOrDefault(ui => ui.ExternalId == user.Id); + if(u == null) + { + u = _db.Users.Add(new DiscordUser()).Entity; + _db.SaveChanges(); + } + return u; + } +} \ No newline at end of file diff --git a/DiscordInterface/Models/Attachment.cs b/DiscordInterface/Models/Attachment.cs new file mode 100644 index 0000000..0ed47e0 --- /dev/null +++ b/DiscordInterface/Models/Attachment.cs @@ -0,0 +1,4 @@ +namespace vassago.DiscordInterface.Models; +using vassago.Models; + +public class DiscordAttachment : Attachment {} \ No newline at end of file diff --git a/DiscordInterface/Models/Channel.cs b/DiscordInterface/Models/Channel.cs new file mode 100644 index 0000000..5791d12 --- /dev/null +++ b/DiscordInterface/Models/Channel.cs @@ -0,0 +1,18 @@ +namespace vassago.DiscordInterface.Models; + +using System.Threading.Tasks; +using vassago.Models; + +public class DiscordChannel : Channel +{ + public override Task SendFile(string path, string messageText = null) + { + throw new System.NotImplementedException(); + } + + public override Task SendMessage(string text) + { + + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/DiscordInterface/Models/Message.cs b/DiscordInterface/Models/Message.cs new file mode 100644 index 0000000..0e562e4 --- /dev/null +++ b/DiscordInterface/Models/Message.cs @@ -0,0 +1,40 @@ +namespace vassago.DiscordInterface.Models; + +using System; +using System.Linq; +using System.Threading.Tasks; +using Discord.WebSocket; +using Newtonsoft.Json; +using vassago.Models; + +public class DiscordMessage : Message +{ + private SocketUserMessage _externalEntity; + + public DiscordMessage(SocketUserMessage suMessage) + { + _externalEntity = suMessage; + } + + public override Task React(string reaction) + { + return _externalEntity.AddReactionAsync(Discord.Emote.Parse(reaction)); + } + + public override Task Reply(string message) + { + return _externalEntity.Channel.SendMessageAsync(message, messageReference: new Discord.MessageReference(_externalEntity.Id)); + } + + internal void Intake(SocketUserMessage suMessage, ulong currentUserId) + { + this.Content = suMessage.Content; + this.ExternalId = suMessage.Id; + this.Timestamp = suMessage.EditedTimestamp ?? suMessage.CreatedAt; + + if (suMessage.MentionedUsers?.FirstOrDefault(muid => muid.Id == currentUserId) != null) + { + this.MentionsMe = true; + } + } +} diff --git a/DiscordInterface/Models/Protocol.cs b/DiscordInterface/Models/Protocol.cs new file mode 100644 index 0000000..e928971 --- /dev/null +++ b/DiscordInterface/Models/Protocol.cs @@ -0,0 +1,20 @@ +namespace vassago.DiscordInterface.Models; +using vassago.Models; +using Discord; +using Discord.WebSocket; +using System.Threading.Tasks; + +public class DiscordProtocol : Protocol +{ + public DiscordSocketClient Client {get;set;} + + public override Task SendFile(string path, string messageText = null) + { + throw new System.InvalidOperationException("can't send a file to \"discord\", pick a channel"); + } + + public override Task SendMessage(string message) + { + throw new System.InvalidOperationException("can't send a message to \"discord\", pick a channel"); + } +} \ No newline at end of file diff --git a/DiscordInterface/Models/User.cs b/DiscordInterface/Models/User.cs new file mode 100644 index 0000000..3552a2a --- /dev/null +++ b/DiscordInterface/Models/User.cs @@ -0,0 +1,6 @@ +namespace vassago.DiscordInterface.Models; +using vassago.Models; +using Discord; +using Discord.WebSocket; + +public class DiscordUser : User { } \ No newline at end of file diff --git a/Discord/SlashCommandsHelper.cs b/DiscordInterface/SlashCommandsHelper.cs similarity index 99% rename from Discord/SlashCommandsHelper.cs rename to DiscordInterface/SlashCommandsHelper.cs index 87c50d4..7dfd77d 100644 --- a/Discord/SlashCommandsHelper.cs +++ b/DiscordInterface/SlashCommandsHelper.cs @@ -7,7 +7,7 @@ using Discord.WebSocket; using Discord; using Discord.Net; -namespace vassago.Discord_Vassago +namespace vassago.DiscordInterface { public static class SlashCommandsHelper diff --git a/Features.cs b/Features.cs index 5e23cfe..26b4c08 100644 --- a/Features.cs +++ b/Features.cs @@ -7,10 +7,12 @@ using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; using Newtonsoft.Json; using QRCoder; -namespace vassago +namespace silverworker_discord { public static class Features { @@ -164,7 +166,7 @@ namespace vassago var thisJoke = jokes[r.Next(jokes.Length)]; if (thisJoke.Contains("?") && !thisJoke.EndsWith('?')) { - #pragma warning disable 4014 +#pragma warning disable 4014 Task.Run(async () => { var firstIndexAfterQuestionMark = thisJoke.LastIndexOf('?') + 1; @@ -178,7 +180,7 @@ namespace vassago await myOwnMsg.AddReactionAsync(new Emoji("\U0001F60E")); //smiling face with sunglasses } }); - #pragma warning restore 4014 +#pragma warning restore 4014 } else { diff --git a/Migrations/20230601033836_initial.Designer.cs b/Migrations/20230601033836_initial.Designer.cs new file mode 100644 index 0000000..ff4d9ec --- /dev/null +++ b/Migrations/20230601033836_initial.Designer.cs @@ -0,0 +1,316 @@ +// +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("20230601033836_initial")] + partial class initial + { + /// + 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("ChannelUser", b => + { + b.Property("OtherUsersId") + .HasColumnType("uuid"); + + b.Property("SeenInChannelsId") + .HasColumnType("uuid"); + + b.HasKey("OtherUsersId", "SeenInChannelsId"); + + b.HasIndex("SeenInChannelsId"); + + b.ToTable("ChannelUser"); + }); + + modelBuilder.Entity("vassago.Models.Attachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Content") + .HasColumnType("bytea"); + + b.Property("ExternalId") + .HasColumnType("numeric(20,0)"); + + b.Property("Filename") + .HasColumnType("text"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("Source") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("Attachments"); + }); + + modelBuilder.Entity("vassago.Models.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasColumnType("numeric(20,0)"); + + b.Property("IsDM") + .HasColumnType("boolean"); + + b.Property("ParentChannelId") + .HasColumnType("uuid"); + + b.Property("PermissionsOverridesId") + .HasColumnType("integer"); + + b.Property("ProtocolId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ParentChannelId"); + + b.HasIndex("PermissionsOverridesId"); + + b.HasIndex("ProtocolId"); + + b.ToTable("Channels"); + + b.HasDiscriminator("Discriminator").HasValue("Channel"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("vassago.Models.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActedOn") + .HasColumnType("boolean"); + + b.Property("AuthorId") + .HasColumnType("uuid"); + + b.Property("ChannelId") + .HasColumnType("uuid"); + + b.Property("Content") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasColumnType("numeric(20,0)"); + + b.Property("ExternalRepresentation") + .HasColumnType("text"); + + b.Property("MentionsMe") + .HasColumnType("boolean"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LewdnessFilterLevel") + .HasColumnType("integer"); + + b.Property("LinksAllowed") + .HasColumnType("boolean"); + + b.Property("MaxAttachmentBytes") + .HasColumnType("bigint"); + + b.Property("MaxTextChars") + .HasColumnType("bigint"); + + b.Property("MeannessFilterLevel") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("PermissionSettings"); + }); + + modelBuilder.Entity("vassago.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("External") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasColumnType("numeric(20,0)"); + + b.Property("IsBot") + .HasColumnType("boolean"); + + b.Property("ProtocolId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProtocolId"); + + b.HasIndex("UserId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("vassago.Models.Protocol", b => + { + b.HasBaseType("vassago.Models.Channel"); + + b.Property("ConnectionToken") + .HasColumnType("text"); + + b.HasDiscriminator().HasValue("Protocol"); + }); + + modelBuilder.Entity("ChannelUser", b => + { + b.HasOne("vassago.Models.User", null) + .WithMany() + .HasForeignKey("OtherUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("vassago.Models.Channel", null) + .WithMany() + .HasForeignKey("SeenInChannelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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", "PermissionsOverrides") + .WithMany() + .HasForeignKey("PermissionsOverridesId"); + + b.HasOne("vassago.Models.Protocol", "Protocol") + .WithMany() + .HasForeignKey("ProtocolId"); + + b.Navigation("ParentChannel"); + + b.Navigation("PermissionsOverrides"); + + b.Navigation("Protocol"); + }); + + 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.Protocol", "Protocol") + .WithMany() + .HasForeignKey("ProtocolId"); + + b.HasOne("vassago.Models.User", null) + .WithMany("KnownAliases") + .HasForeignKey("UserId"); + + b.Navigation("Protocol"); + }); + + modelBuilder.Entity("vassago.Models.Channel", b => + { + b.Navigation("Messages"); + + b.Navigation("SubChannels"); + }); + + modelBuilder.Entity("vassago.Models.Message", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("vassago.Models.User", b => + { + b.Navigation("KnownAliases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20230601033836_initial.cs b/Migrations/20230601033836_initial.cs new file mode 100644 index 0000000..97dbc1c --- /dev/null +++ b/Migrations/20230601033836_initial.cs @@ -0,0 +1,235 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace vassago.Migrations +{ + /// + public partial class initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "PermissionSettings", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MaxAttachmentBytes = table.Column(type: "bigint", nullable: true), + MaxTextChars = table.Column(type: "bigint", nullable: true), + LinksAllowed = table.Column(type: "boolean", nullable: true), + LewdnessFilterLevel = table.Column(type: "integer", nullable: true), + MeannessFilterLevel = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PermissionSettings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Channels", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ExternalId = table.Column(type: "numeric(20,0)", nullable: true), + DisplayName = table.Column(type: "text", nullable: true), + IsDM = table.Column(type: "boolean", nullable: false), + PermissionsOverridesId = table.Column(type: "integer", nullable: true), + ParentChannelId = table.Column(type: "uuid", nullable: true), + ProtocolId = table.Column(type: "uuid", nullable: true), + Discriminator = table.Column(type: "text", nullable: false), + ConnectionToken = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Channels", x => x.Id); + table.ForeignKey( + name: "FK_Channels_Channels_ParentChannelId", + column: x => x.ParentChannelId, + principalTable: "Channels", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Channels_Channels_ProtocolId", + column: x => x.ProtocolId, + principalTable: "Channels", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Channels_PermissionSettings_PermissionsOverridesId", + column: x => x.PermissionsOverridesId, + principalTable: "PermissionSettings", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ExternalId = table.Column(type: "numeric(20,0)", nullable: true), + DisplayName = table.Column(type: "text", nullable: true), + IsBot = table.Column(type: "boolean", nullable: false), + ProtocolId = table.Column(type: "uuid", nullable: true), + External = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_Channels_ProtocolId", + column: x => x.ProtocolId, + principalTable: "Channels", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Users_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ChannelUser", + columns: table => new + { + OtherUsersId = table.Column(type: "uuid", nullable: false), + SeenInChannelsId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ChannelUser", x => new { x.OtherUsersId, x.SeenInChannelsId }); + table.ForeignKey( + name: "FK_ChannelUser_Channels_SeenInChannelsId", + column: x => x.SeenInChannelsId, + principalTable: "Channels", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ChannelUser_Users_OtherUsersId", + column: x => x.OtherUsersId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Messages", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ExternalId = table.Column(type: "numeric(20,0)", nullable: true), + Content = table.Column(type: "text", nullable: true), + MentionsMe = table.Column(type: "boolean", nullable: false), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false), + ActedOn = table.Column(type: "boolean", nullable: false), + ExternalRepresentation = table.Column(type: "text", nullable: true), + AuthorId = table.Column(type: "uuid", nullable: true), + ChannelId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Messages", x => x.Id); + table.ForeignKey( + name: "FK_Messages_Channels_ChannelId", + column: x => x.ChannelId, + principalTable: "Channels", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Messages_Users_AuthorId", + column: x => x.AuthorId, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Attachments", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ExternalId = table.Column(type: "numeric(20,0)", nullable: true), + Source = table.Column(type: "text", nullable: true), + Content = table.Column(type: "bytea", nullable: true), + Filename = table.Column(type: "text", nullable: true), + MessageId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Attachments", x => x.Id); + table.ForeignKey( + name: "FK_Attachments_Messages_MessageId", + column: x => x.MessageId, + principalTable: "Messages", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Attachments_MessageId", + table: "Attachments", + column: "MessageId"); + + migrationBuilder.CreateIndex( + name: "IX_Channels_ParentChannelId", + table: "Channels", + column: "ParentChannelId"); + + migrationBuilder.CreateIndex( + name: "IX_Channels_PermissionsOverridesId", + table: "Channels", + column: "PermissionsOverridesId"); + + migrationBuilder.CreateIndex( + name: "IX_Channels_ProtocolId", + table: "Channels", + column: "ProtocolId"); + + migrationBuilder.CreateIndex( + name: "IX_ChannelUser_SeenInChannelsId", + table: "ChannelUser", + column: "SeenInChannelsId"); + + migrationBuilder.CreateIndex( + name: "IX_Messages_AuthorId", + table: "Messages", + column: "AuthorId"); + + migrationBuilder.CreateIndex( + name: "IX_Messages_ChannelId", + table: "Messages", + column: "ChannelId"); + + migrationBuilder.CreateIndex( + name: "IX_Users_ProtocolId", + table: "Users", + column: "ProtocolId"); + + migrationBuilder.CreateIndex( + name: "IX_Users_UserId", + table: "Users", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Attachments"); + + migrationBuilder.DropTable( + name: "ChannelUser"); + + migrationBuilder.DropTable( + name: "Messages"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Channels"); + + migrationBuilder.DropTable( + name: "PermissionSettings"); + } + } +} diff --git a/Migrations/ChattingContextModelSnapshot.cs b/Migrations/ChattingContextModelSnapshot.cs new file mode 100644 index 0000000..a97dea4 --- /dev/null +++ b/Migrations/ChattingContextModelSnapshot.cs @@ -0,0 +1,313 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using vassago.Models; + +#nullable disable + +namespace vassago.Migrations +{ + [DbContext(typeof(ChattingContext))] + partial class ChattingContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ChannelUser", b => + { + b.Property("OtherUsersId") + .HasColumnType("uuid"); + + b.Property("SeenInChannelsId") + .HasColumnType("uuid"); + + b.HasKey("OtherUsersId", "SeenInChannelsId"); + + b.HasIndex("SeenInChannelsId"); + + b.ToTable("ChannelUser"); + }); + + modelBuilder.Entity("vassago.Models.Attachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Content") + .HasColumnType("bytea"); + + b.Property("ExternalId") + .HasColumnType("numeric(20,0)"); + + b.Property("Filename") + .HasColumnType("text"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("Source") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MessageId"); + + b.ToTable("Attachments"); + }); + + modelBuilder.Entity("vassago.Models.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasColumnType("numeric(20,0)"); + + b.Property("IsDM") + .HasColumnType("boolean"); + + b.Property("ParentChannelId") + .HasColumnType("uuid"); + + b.Property("PermissionsOverridesId") + .HasColumnType("integer"); + + b.Property("ProtocolId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ParentChannelId"); + + b.HasIndex("PermissionsOverridesId"); + + b.HasIndex("ProtocolId"); + + b.ToTable("Channels"); + + b.HasDiscriminator("Discriminator").HasValue("Channel"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("vassago.Models.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActedOn") + .HasColumnType("boolean"); + + b.Property("AuthorId") + .HasColumnType("uuid"); + + b.Property("ChannelId") + .HasColumnType("uuid"); + + b.Property("Content") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasColumnType("numeric(20,0)"); + + b.Property("ExternalRepresentation") + .HasColumnType("text"); + + b.Property("MentionsMe") + .HasColumnType("boolean"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LewdnessFilterLevel") + .HasColumnType("integer"); + + b.Property("LinksAllowed") + .HasColumnType("boolean"); + + b.Property("MaxAttachmentBytes") + .HasColumnType("bigint"); + + b.Property("MaxTextChars") + .HasColumnType("bigint"); + + b.Property("MeannessFilterLevel") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("PermissionSettings"); + }); + + modelBuilder.Entity("vassago.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("External") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasColumnType("numeric(20,0)"); + + b.Property("IsBot") + .HasColumnType("boolean"); + + b.Property("ProtocolId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProtocolId"); + + b.HasIndex("UserId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("vassago.Models.Protocol", b => + { + b.HasBaseType("vassago.Models.Channel"); + + b.Property("ConnectionToken") + .HasColumnType("text"); + + b.HasDiscriminator().HasValue("Protocol"); + }); + + modelBuilder.Entity("ChannelUser", b => + { + b.HasOne("vassago.Models.User", null) + .WithMany() + .HasForeignKey("OtherUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("vassago.Models.Channel", null) + .WithMany() + .HasForeignKey("SeenInChannelsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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", "PermissionsOverrides") + .WithMany() + .HasForeignKey("PermissionsOverridesId"); + + b.HasOne("vassago.Models.Protocol", "Protocol") + .WithMany() + .HasForeignKey("ProtocolId"); + + b.Navigation("ParentChannel"); + + b.Navigation("PermissionsOverrides"); + + b.Navigation("Protocol"); + }); + + 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.Protocol", "Protocol") + .WithMany() + .HasForeignKey("ProtocolId"); + + b.HasOne("vassago.Models.User", null) + .WithMany("KnownAliases") + .HasForeignKey("UserId"); + + b.Navigation("Protocol"); + }); + + modelBuilder.Entity("vassago.Models.Channel", b => + { + b.Navigation("Messages"); + + b.Navigation("SubChannels"); + }); + + modelBuilder.Entity("vassago.Models.Message", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("vassago.Models.User", b => + { + b.Navigation("KnownAliases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Models/Attachment.cs b/Models/Attachment.cs index 51a95f3..b6bae55 100644 --- a/Models/Attachment.cs +++ b/Models/Attachment.cs @@ -1,5 +1,12 @@ +namespace vassago.Models; + using System; -public abstract class Attachment +public class Attachment { - public Guid Id{get;set;} + public Guid Id { get; set; } + public ulong? ExternalId { get; set; } + public Uri Source { get; set; } + public byte[] Content { get; set; } + public string Filename { get; set; } + public Message Message { get; set; } } \ No newline at end of file diff --git a/Models/Channel.cs b/Models/Channel.cs index fc0656e..31027c6 100644 --- a/Models/Channel.cs +++ b/Models/Channel.cs @@ -1,7 +1,28 @@ -using System; +namespace vassago.Models; -public abstract class Channel +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +public class Channel { - public Guid Id{get;set;} - public bool IsDM {get;set;} -} \ No newline at end of file + public Guid Id { get; set; } + public ulong? ExternalId { get; set; } + public string DisplayName { get; set; } + public bool IsDM { get; set; } + public IEnumerable OtherUsers { get; set; } + public PermissionSettings PermissionsOverrides { get; set; } + public IEnumerable SubChannels { get; set; } + public Channel ParentChannel { get; set; } + public Protocol Protocol { get; set; } + public IEnumerable Messages { get; set; } + + public virtual Task SendMessage(string text) + { + throw new NotImplementedException("derive from me"); + } + public virtual Task SendFile(string path, string messageText = null) + { + throw new NotImplementedException("derive from me"); + } +} diff --git a/Models/ChattingContext.cs b/Models/ChattingContext.cs new file mode 100644 index 0000000..538f187 --- /dev/null +++ b/Models/ChattingContext.cs @@ -0,0 +1,17 @@ +namespace vassago.Models; + +using Microsoft.EntityFrameworkCore; + +public class ChattingContext : DbContext +{ + public DbSet Attachments { get; set; } + public DbSet Channels { get; set; } + //public DbSet Emoji {get;set;} + public DbSet Messages { get; set; } + public DbSet PermissionSettings{get;set;} + public DbSet Protocols { get; set; } + public DbSet Users { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseNpgsql(Shared.DBConnectionString); +} \ No newline at end of file diff --git a/Models/Enums.cs b/Models/Enums.cs new file mode 100644 index 0000000..d392ede --- /dev/null +++ b/Models/Enums.cs @@ -0,0 +1,33 @@ +namespace vassago.Models; + +public static class Enumerations +{ + public static string LewdnessFilterLevel(int level) + { + switch (level) + { + case 0: + return "this is a christian minecraft server 🙏"; + case 1: + return "G-Rated"; + case 2: + return "polite company"; + case 3: + return ";) ;) ;)"; + default: + return "ERROR"; + } + } + public static string MeannessFilterLevel(int level) + { + switch (level) + { + case 0: + return "good vibes only"; + case 1: + return "387.44 million miles of printed circuits, etc"; + default: + return "ERROR"; + } + } +} \ No newline at end of file diff --git a/Models/Message.cs b/Models/Message.cs index 029342e..facb2f3 100644 --- a/Models/Message.cs +++ b/Models/Message.cs @@ -1,9 +1,30 @@ -using System; +namespace vassago.Models; -public abstract class Message +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Discord.WebSocket; + +public class Message { - public Guid Id{get;set;} - public Guid InChannel{get;set;} - public string Content{get;set;} - public bool TagsMe{get;set;} -} \ No newline at end of file + public Guid Id { get; set; } + public ulong? ExternalId { get; set; } + public string Content { get; set; } + public bool MentionsMe { get; set; } + public DateTimeOffset Timestamp { get; set; } + public bool ActedOn { get; set; } + ///however it came from the protocol. + public string ExternalRepresentation { get; set; } + public IEnumerable Attachments { get; set; } + public User Author { get; set; } + public Channel Channel { get; set; } + + public virtual Task Reply(string message) + { + throw new NotImplementedException("derive from me"); + } + public virtual Task React(string reaction) + { + throw new NotImplementedException("derive from me"); + } +} diff --git a/Models/PermissionSettings.cs b/Models/PermissionSettings.cs new file mode 100644 index 0000000..f209cde --- /dev/null +++ b/Models/PermissionSettings.cs @@ -0,0 +1,12 @@ +namespace vassago.Models; + +using System; +public class PermissionSettings +{ + public int Id { get; set; } + public uint? MaxAttachmentBytes { get; set; } + public uint? MaxTextChars { get; set; } + public bool? LinksAllowed { get; set; } + public int? LewdnessFilterLevel { get; set; } + public int? MeannessFilterLevel { get; set; } +} diff --git a/Models/Protocol.cs b/Models/Protocol.cs index bfc96cb..a67ce10 100644 --- a/Models/Protocol.cs +++ b/Models/Protocol.cs @@ -1,6 +1,13 @@ -using System; +namespace vassago.Models; -public abstract class Protocol +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +public class Protocol : Channel { - public Guid Id{get;set;} + //log in, log out, observe events? + + //doesn't actually have to be a token, but it should be how an interface can find itself in the DB + public string ConnectionToken { get; set; } } diff --git a/Models/User.cs b/Models/User.cs index 96011c4..45ff169 100644 --- a/Models/User.cs +++ b/Models/User.cs @@ -1,7 +1,16 @@ -using System; +namespace vassago.Models; -public abstract class User +using System; +using System.Collections.Generic; + +public class User //more like "user's account - no concept of the person outside of the protocol. (yet?) { - public Guid Id{get;set;} - public bool IsBot {get;set;} //webhook counts + public Guid Id { get; set; } + public ulong? ExternalId { get; set; } + public string DisplayName { get; set; } + public bool IsBot { get; set; } //webhook counts + public IEnumerable SeenInChannels { get; set; } + public IEnumerable KnownAliases { get; set; } + public Protocol Protocol { get; set; } + public string External { get; set; } } \ No newline at end of file diff --git a/Program.cs b/Program.cs index 179e526..eae45da 100644 --- a/Program.cs +++ b/Program.cs @@ -9,24 +9,29 @@ using Newtonsoft.Json; using System.Text; using System.Threading; using System.Diagnostics; -using vassago.Discord_Vassago; +using vassago.Models; namespace vassago { class Program { Configuration config = Configuration.Parse("appsettings.json"); - private List discords = new List(); + private List discords = new List(); public static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); public async Task MainAsync() { + Shared.DBConnectionString = config.DBConnectionString; + Shared.dbContext = new ChattingContext(); + { + Shared.dbContext.Database.EnsureCreated(); + } Conversion.Converter.Load(config.ExchangePairsLocation); if(config.DiscordTokens.Any()) foreach(var dt in config.DiscordTokens) { - var d = new DiscordInterface(); + var d = new DiscordInterface.DiscordInterface(); await d.Init(dt); discords.Add(d); } diff --git a/Shared.cs b/Shared.cs index 2054982..921847b 100644 --- a/Shared.cs +++ b/Shared.cs @@ -1,10 +1,14 @@ +namespace vassago; using System; +using System.Net.Http; +using vassago.Models; -namespace vassago + +public static class Shared { - public static class Shared - { - public static Random r = new Random(); - } -} \ No newline at end of file + public static Random r = new Random(); + public static string DBConnectionString { get; set; } + public static ChattingContext dbContext { get; set; } + public static HttpClient HttpClient { get; internal set; } = new HttpClient(); +} diff --git a/SlashCommandsHelper.cs b/SlashCommandsHelper.cs new file mode 100644 index 0000000..2efb419 --- /dev/null +++ b/SlashCommandsHelper.cs @@ -0,0 +1,149 @@ +namespace silverworker_discord +{ + using System; + using System.Linq; + using System.Collections.Generic; + using System.Threading.Tasks; + using Discord.WebSocket; + using Discord.Net; + using Discord; + using Newtonsoft.Json; + + public static class SlashCommandsHelper + { + private static List slashCommands = new List() + { + new CommandSetup(){ + Id = "freedomunits", + UpdatedAt = new DateTime(2023, 5, 21, 13, 3, 0), + guild = 825293851110801428, + register = register_FreedomUnits + } + }; + public static async Task Register(DiscordSocketClient client) + { + var commandsInContext = await client.GetGlobalApplicationCommandsAsync(); + await Register(client, commandsInContext, null); + foreach (var guild in client.Guilds) + { + try + { + await Register(client, await guild.GetApplicationCommandsAsync(), guild); + } + catch (Discord.Net.HttpException ex) + { + Console.Error.WriteLine($"error registering slash commands for guild {guild.Name} (id {guild.Id}) - {ex.Message}"); + } + } + } + + private static async Task Register(DiscordSocketClient client, IEnumerable commandsInContext, SocketGuild guild) + { + foreach (var existingCommand in commandsInContext) + { + var myVersion = slashCommands.FirstOrDefault(c => c.Id == existingCommand.Name && c.guild == guild?.Id); + if (myVersion == null) + { + Console.WriteLine($"deleting command {existingCommand.Name} - (created at {existingCommand.CreatedAt}, it's in guild {existingCommand.Guild?.Id} while I'm in {guild?.Id})"); + await existingCommand.DeleteAsync(); + Console.WriteLine("survived"); + } + else + { + Console.WriteLine(existingCommand.CreatedAt); + if (myVersion.UpdatedAt > existingCommand.CreatedAt) + { + Console.WriteLine($"overwriting command {existingCommand.Name}"); + await myVersion.register(false, client, guild); + Console.WriteLine($"survived"); + } + myVersion.alreadyRegistered = true; + } + } + foreach (var remaining in slashCommands.Where(sc => sc.alreadyRegistered == false && sc.guild == guild?.Id)) + { + Console.WriteLine($"creating new command {remaining.Id} ({(remaining.guild == null ? "global" : $"for guild {remaining.guild}")})"); + await remaining.register(true, client, guild); + Console.WriteLine($"survived"); + } + } + + private static async Task register_FreedomUnits(bool isNew, DiscordSocketClient client, SocketGuild guild) + { + var builtCommand = new SlashCommandBuilder() + .WithName("freedomunits") + .WithDescription("convert between misc units (currency: iso 4217 code)") + .AddOption("amount", ApplicationCommandOptionType.Number, "source amount", isRequired: true) + .AddOption(new SlashCommandOptionBuilder() + .WithName("src-unit") + .WithDescription("unit converting FROM") + .WithRequired(true) + .WithType(ApplicationCommandOptionType.String)) + .AddOption(new SlashCommandOptionBuilder() + .WithName("dest-unit") + .WithDescription("unit converting TO") + .WithRequired(true) + .WithType(ApplicationCommandOptionType.String)) + .Build(); + try + { + if (guild != null) + { + if (isNew) + await guild.CreateApplicationCommandAsync(builtCommand); + else + await guild.BulkOverwriteApplicationCommandAsync(new ApplicationCommandProperties[] { builtCommand }); + } + else + { + if (isNew) + await client.CreateGlobalApplicationCommandAsync(builtCommand); + else + await client.BulkOverwriteGlobalApplicationCommandsAsync(new ApplicationCommandProperties[] { builtCommand }); + } + } + catch (HttpException exception) + { + var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented); + Console.Error.WriteLine(json); + } + } + public static async Task SlashCommandHandler(SocketSlashCommand command) + { + switch(command.CommandName) + { + case "freedomunits": + try + { + var amt = Convert.ToDecimal((double)(command.Data.Options.First(o => o.Name == "amount").Value)); + var src = (string)command.Data.Options.First(o => o.Name == "src-unit").Value; + var dest = (string)command.Data.Options.First(o => o.Name == "dest-unit").Value; + var conversionResult = Conversion.Converter.Convert(amt, src, dest); + + await command.RespondAsync($"> {amt} {src} -> {dest}\n{conversionResult}"); + } + catch(Exception e) + { + await command.RespondAsync($"error: {e.Message}. aaadam!"); + } + break; + default: + await command.RespondAsync($"\\*smiles and nods*\n"); + await command.Channel.SendFileAsync($"assets/loud sweating.gif"); + Console.Error.WriteLine($"can't understand command name: {command.CommandName}"); + break; + } + } + private class CommandSetup + { + public string Id { get; set; } + //the date/time you updated yours IN UTC. + public DateTimeOffset UpdatedAt { get; set; } + public Registration register { get; set; } + public ulong? guild { get; set; } + public bool alreadyRegistered {get;set; } = false; + + public delegate Task Registration(bool isNew, DiscordSocketClient client, SocketGuild guild); + } + } +} \ No newline at end of file diff --git a/appsettings.example.json b/appsettings.example.json new file mode 100644 index 0000000..b06d933 --- /dev/null +++ b/appsettings.example.json @@ -0,0 +1,4 @@ +{ + "token": "59 chars", + "exchangePairsLocation": "assets/exchangepairs.json" +} \ No newline at end of file diff --git a/vassago.csproj b/vassago.csproj index 9ec633c..acb4cd6 100644 --- a/vassago.csproj +++ b/vassago.csproj @@ -1,16 +1,22 @@ - + Exe net7.0 vassago + 1b425139-0de4-443f-91e6-a8eec44855dd + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + @@ -32,7 +38,7 @@ Always - Never + Always