using System.Diagnostics; using System.Text; using System.Text.RegularExpressions; using Discord; using Discord.WebSocket; using DiscordRPC; using Microsoft.Extensions.Configuration; using RestSharp; using Terminal.Gui; using Terminal.Gui.Trees; using Attribute = Terminal.Gui.Attribute; using Button = Terminal.Gui.Button; using Color = Terminal.Gui.Color; namespace chord { public class Program { private ListView chatBoxList; private List currentChannelMessages; private ulong currentSelectedChannel; private ulong currentSelectedGuild; private SortedDictionary> guilds; private DiscordRpcClient rpcClient; private Settings settings; private Window window; public static void Main() { new Program().Start(); } private async void Start() { try { #if DEBUG var config = new ConfigurationBuilder() .AddJsonFile("config.json") .Build(); #else var config = new ConfigurationBuilder() .AddJsonFile($"{Environment.GetEnvironmentVariable("HOME")}/.config/chord/config.json") .Build(); #endif settings = config.Get(); var client = new DiscordSocketClient(new DiscordSocketConfig { AlwaysDownloadUsers = true, GatewayIntents = GatewayIntents.All }); await client.LoginAsync(TokenType.Bot, settings.Token); await client.StartAsync(); if (settings.EnableRichPresence) { rpcClient = new DiscordRpcClient("923436807297859625"); rpcClient.Initialize(); rpcClient.SetPresence(new RichPresence()); } Application.Init(); window = new Window("chord") { X = 0, Y = 1, Width = Dim.Fill(), Height = Dim.Fill(), ColorScheme = { Normal = new Attribute(Color.White, Color.Black) } }; var menuBar = buildMenu(); var loadingLabel = new Label("Loading...") { X = 0, Y = 0, Width = Dim.Fill(), Height = Dim.Fill(), TextAlignment = TextAlignment.Centered, VerticalTextAlignment = VerticalTextAlignment.Middle, }; client.Ready += () => { guilds = new SortedDictionary>(); foreach (var guild in client.Guilds) { guilds.Add(guild.Id, new List()); foreach (var channel in guild.TextChannels) { var findGuild = guilds.GetValueOrDefault(guild.Id); if (channel.Users.ToList().Find(user => user.Id == client.CurrentUser.Id) == null) continue; if (findGuild == null) continue; findGuild.Add(channel.Id); findGuild.Sort(); } } var serverList = buildServerList(); var channelList = buildChannelList(serverList); var chatBox = buildChatBox(serverList); var messageBox = buildMessageBox(serverList, chatBox); var userList = buildUserList(chatBox); var serverListList = new ListView { X = 0, Y = 0, Width = Dim.Fill(), Height = Dim.Fill() }; var guildNames = guilds.Select(guild => client.GetGuild(guild.Key).Name).ToList(); serverListList.SetSource(guildNames); var channelListTree = new TreeView { X = 0, Y = 0, Width = Dim.Fill(), Height = Dim.Fill() }; chatBoxList = new ListView { X = 0, Y = 0, Width = Dim.Fill(), Height = Dim.Fill() }; var messageBoxText = new TextView { X = 0, Y = 0, Width = Dim.Percent(95), Height = Dim.Fill() }; var messageBoxSend = new Button("Send", true) { X = Pos.Right(messageBoxText) - 2, Y = 0 }; var userListTree = new TreeView { X = 0, Y = 0, Width = Dim.Fill(), Height = Dim.Fill() }; serverListList.OpenSelectedItem += args => { channelListTree.ClearObjects(); currentSelectedGuild = guilds.Keys.ToList()[args.Item]; var channelNames = guilds.GetValueOrDefault(currentSelectedGuild)! .Select(channel => client.GetGuild(currentSelectedGuild).GetTextChannel(channel).Name).ToList(); var categoryDict = new SortedDictionary(); foreach (var categories in client.GetGuild(currentSelectedGuild).CategoryChannels) { var categoryName = Regex.Replace(categories.Name, @"[^\u0000-\u007F]+", string.Empty); var node = new TreeNode(categoryName); foreach(var channel in categories.Channels) { if (channel.GetType().Name == "SocketTextChannel") { var channelName = Regex.Replace(channel.Name, @"[^\u0000-\u007F]+", string.Empty); var channelNode = new TreeNode(channelName); channelNode.Tag = new { Id = channel.Id }; node.Children.Add(channelNode); } } categoryDict.Add(categories.Id, node); } foreach (var category in categoryDict) { channelListTree.AddObject(category.Value); } if (settings.EnableRichPresence) { rpcClient.UpdateDetails($"Chatting in {client.GetGuild(currentSelectedGuild).Name}"); } }; channelListTree.SelectionChanged += async (arg1, arg2) => { userListTree.ClearObjects(); try { currentSelectedChannel = (ulong)arg2.NewValue.Tag.GetType().GetProperty("Id").GetValue(arg2.NewValue.Tag); } catch { return; } try { var restSharpClient = new RestClient("https://discord.com/api/v9"); var restSharpReq = new RestRequest($"channels/{currentSelectedChannel}/messages"); restSharpClient.AddDefaultHeader("Authorization", settings.Token); var response = await restSharpClient.GetAsync>(restSharpReq); var messages = new List(); foreach (var msg in response) { var msgNewLines = msg.content.Split("\n").ToList(); var firstMsg = msgNewLines[0]; msgNewLines.RemoveAt(0); msgNewLines.Reverse(); messages.AddRange(msgNewLines.Select(message => $"{message}")); messages.Add($"{msg.author.username}#{msg.author.discriminator} | {firstMsg}"); if (msg.embeds.Count != 0) messages.Add($"{msg.author.username}#{msg.author.discriminator} | [Unable to display embed]"); if (msg.attachments.Count == 0) continue; messages.AddRange(msg.attachments .Select(attachment => $"{msg.author.username}#{msg.author.discriminator} | {attachment.url}") .Select(dummy => dummy)); } messages.Reverse(); await chatBoxList.SetSourceAsync(messages); currentChannelMessages = messages; chatBoxList.ScrollDown(currentChannelMessages.Count - chatBoxList.Bounds.Height); chatBoxList.SelectedItem = currentChannelMessages.Count - 1; var rolesDict = new SortedDictionary(); foreach (var roles in client.GetGuild(currentSelectedGuild).Roles) { if (roles.IsHoisted) { if (roles.Members.Count() != 0) { var roleName = Regex.Replace(roles.Name, @"[^\u0000-\u007F]+", string.Empty); var node = new TreeNode(roleName); foreach (var users in roles.Members) { var userName = Regex.Replace(users.Username, @"[^\u0000-\u007F]+", string.Empty); string userNick = string.Empty; if (users.Nickname != null) userNick = Regex.Replace(users.Nickname, @"[^\u0000-\u007F]+", string.Empty); node.Children.Add(new TreeNode($"{userName}#{users.Discriminator} ({userNick})")); } rolesDict.TryAdd(Math.Abs(roles.Position - client.GetGuild(currentSelectedGuild).Roles.Count()), node); } } } foreach (var role in rolesDict) { userListTree.AddObject(role.Value); } if (settings.EnableRichPresence) { rpcClient.UpdateState($"In channel {client.GetGuild(currentSelectedGuild).GetTextChannel(currentSelectedChannel).Name}"); rpcClient.UpdateStartTime(); } } catch(Exception err) { Console.WriteLine(err); await chatBoxList.SetSourceAsync(new List()); } }; messageBoxSend.Clicked += async () => { var restSharpClient = new RestClient("https://discord.com/api/v9"); var restSharpReq = new RestRequest($"channels/{currentSelectedChannel}/messages"); restSharpClient.AddDefaultHeader("Authorization", settings.Token); restSharpReq.AddJsonBody(new { content = Encoding.UTF8.GetString(messageBoxText.Text.ToByteArray()) }); await restSharpClient.PostAsync>(restSharpReq); messageBoxText.Text = ""; }; chatBoxList.OpenSelectedItem += args => { if (currentChannelMessages[args.Item].Split(" | ")[1].Contains("http")) for (var i = 0; i < currentChannelMessages[args.Item].Split(" | ")[1].Split(" ").Count(); i++) if (currentChannelMessages[args.Item].Split(" | ")[1].Split(" ")[i].Contains("http")) { var procStartInfo = new ProcessStartInfo("/bin/sh", $"-c \"xdg-open {currentChannelMessages[args.Item].Split(" | ")[1].Split(" ")[i]}\"") { RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, UseShellExecute = false }; var proc = new Process(); proc.StartInfo = procStartInfo; proc.Start(); } }; Application.Top.KeyPress += async args => { switch (ShortcutHelper.GetModifiersKey(args.KeyEvent)) { case Key.CtrlMask | Key.S: serverListList.SetFocus(); break; case Key.CtrlMask | Key.C: channelListTree.SetFocus(); break; case Key.CtrlMask | Key.AltMask | Key.C: chatBoxList.SetFocus(); break; case Key.CtrlMask | Key.U: userListTree.SetFocus(); break; case Key.CtrlMask | Key.Enter: var restSharpClient = new RestClient("https://discord.com/api/v9"); var restSharpReq = new RestRequest($"channels/{currentSelectedChannel}/messages"); restSharpClient.AddDefaultHeader("Authorization", settings.Token); restSharpReq.AddJsonBody(new { content = Encoding.UTF8.GetString(messageBoxText.Text.ToByteArray()) }); await restSharpClient.PostAsync>(restSharpReq); messageBoxText.Text = ""; break; } }; window.RemoveAll(); window.Add(serverList, channelList, messageBox, chatBox, userList); serverList.Add(serverListList); channelList.Add(channelListTree); chatBox.Add(chatBoxList); messageBox.Add(messageBoxText, messageBoxSend); userList.Add(userListTree); return Task.CompletedTask; }; client.MessageReceived += async msg => { if (msg.Channel.Id != currentSelectedChannel) return; var restSharpClient = new RestClient("https://discord.com/api/v9"); var restSharpReq = new RestRequest($"channels/{currentSelectedChannel}/messages"); restSharpReq.AddQueryParameter("limit", "1"); restSharpClient.AddDefaultHeader("Authorization", settings.Token); var response = await restSharpClient.GetAsync>(restSharpReq); currentChannelMessages.Add( $"{response.First().author.username}#{response.First().author.discriminator} | {response.First().content}"); await chatBoxList.SetSourceAsync(currentChannelMessages); chatBoxList.ScrollDown(currentChannelMessages.Count - chatBoxList.Bounds.Height); chatBoxList.SelectedItem = currentChannelMessages.Count - 1; }; Application.MainLoop.AddTimeout(TimeSpan.FromMilliseconds(100), caller => { if (settings.EnableRichPresence) rpcClient.Invoke(); return true; }); Application.Top.Add(window, menuBar); window.Add(loadingLabel); Application.Run(); Application.Shutdown(); } catch (System.Exception err) { Console.WriteLine(err); Application.Shutdown(); } } private Window buildChatBox(Window serverList) { return new Window("Chat Box") { X = Pos.Right(serverList), Y = 0, Width = Dim.Percent(60), Height = Dim.Percent(90) }; } private Window buildChannelList(Window serverList) { return new Window("Channel List") { X = 0, Y = Pos.Bottom(serverList), Width = Dim.Percent(30), Height = Dim.Percent(50) }; } private MenuBar buildMenu() { return new MenuBar(new[] { new MenuBarItem("File", new[] { new MenuItem("_Quit", "Quit the Application", () => { Application.Top.Running = false; }) }), new MenuBarItem("Help", new[] { new MenuItem("_License", "See the Application License", () => { MessageBox.Query("License", "chord Copyright (C) 2021 Daryl Ronningen\nThis program comes with ABSOLUTELY NO WARRANTY; for details see section `15` of the GPL license\nThis is free software, and you are welcome to redistribute it under certain conditions; for details see section `2` of the GPL license", "Close"); }) }) }); } private Window buildMessageBox(Window serverList, Window chatBox) { return new Window("Message Box") { X = Pos.Right(serverList), Y = Pos.Bottom(chatBox), Width = Dim.Percent(60), Height = Dim.Percent(10) }; } private Window buildUserList(Window chatBox) { return new Window("User List") { X = Pos.Right(chatBox), Y = 0, Width = Dim.Percent(10), Height = Dim.Fill() }; } private Window buildServerList() { return new Window("Server List") { X = 0, Y = 0, Width = Dim.Percent(30), Height = Dim.Percent(50) }; } } } public class Settings { public string Token { get; set; } public bool EnableRichPresence { get; set; } } public class GetMessagesResponse { public string content { get; set; } public MessageAuthor author { get; set; } public List embeds { get; set; } public List attachments { get; set; } } public class MessageAuthor { public string username { get; set; } public string discriminator { get; set; } } public class MessageAttachments { public string url { get; set; } }