From e1489d4aa70b96eb386dae283f207c0b111dba02 Mon Sep 17 00:00:00 2001 From: 1whatleytay <1whatleytay@hdsb.ca> Date: Sun, 28 Apr 2019 20:07:29 -0400 Subject: [PATCH] First commit. --- .gitignore | 6 ++ Vita3KBot.sln | 16 ++++ Vita3KBot/Bot.cs | 51 ++++++++++ Vita3KBot/Commands/Compatibility.cs | 144 ++++++++++++++++++++++++++++ Vita3KBot/Commands/Debug.cs | 17 ++++ Vita3KBot/MessageHandler.cs | 81 ++++++++++++++++ Vita3KBot/Utils.cs | 31 ++++++ Vita3KBot/Vita3KBot.csproj | 14 +++ 8 files changed, 360 insertions(+) create mode 100644 .gitignore create mode 100644 Vita3KBot.sln create mode 100644 Vita3KBot/Bot.cs create mode 100644 Vita3KBot/Commands/Compatibility.cs create mode 100644 Vita3KBot/Commands/Debug.cs create mode 100644 Vita3KBot/MessageHandler.cs create mode 100644 Vita3KBot/Utils.cs create mode 100644 Vita3KBot/Vita3KBot.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad498ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea +Vita3KBot/obj +Vita3KBot/bin +Vita3KBot/token.txt +Vita3KBot.sln.DotSettings.user +Vita3KBot.sln.DotSettings diff --git a/Vita3KBot.sln b/Vita3KBot.sln new file mode 100644 index 0000000..e48cc02 --- /dev/null +++ b/Vita3KBot.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vita3KBot", "Vita3KBot\Vita3KBot.csproj", "{15AA3D43-CB5E-4F80-B03C-EAE8B1E40F2E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {15AA3D43-CB5E-4F80-B03C-EAE8B1E40F2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15AA3D43-CB5E-4F80-B03C-EAE8B1E40F2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15AA3D43-CB5E-4F80-B03C-EAE8B1E40F2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15AA3D43-CB5E-4F80-B03C-EAE8B1E40F2E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Vita3KBot/Bot.cs b/Vita3KBot/Bot.cs new file mode 100644 index 0000000..f624f67 --- /dev/null +++ b/Vita3KBot/Bot.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; + +using Discord; +using Discord.Commands; +using Discord.WebSocket; + +using Microsoft.Extensions.DependencyInjection; + +namespace Vita3KBot { + public class Bot { + private readonly string _token; + + private DiscordSocketClient _client; + private MessageHandler _handler; + + // Initializes Discord.Net + private async Task Start() { + _client = new DiscordSocketClient(); + _handler = new MessageHandler(_client); + + await _handler.Init(); + + await _client.LoginAsync(TokenType.Bot, _token); + await _client.StartAsync(); + + await Task.Delay(-1); + } + + private Bot(string token) { + _token = token; + } + + public static void Main(string[] args) { + // Init command with token. + if (args.Length >= 2 && args[0] == "init") { + File.WriteAllText("token.txt", args[1]); + } + + // Start bot with token from "token.txt" in working folder. + try { + var bot = new Bot(File.ReadAllText("token.txt")); + bot.Start().GetAwaiter().GetResult(); + } catch (IOException e) { + Console.WriteLine("Could not read from token.txt. Did you run `init `?"); + } + } + } +} \ No newline at end of file diff --git a/Vita3KBot/Commands/Compatibility.cs b/Vita3KBot/Commands/Compatibility.cs new file mode 100644 index 0000000..b5572a8 --- /dev/null +++ b/Vita3KBot/Commands/Compatibility.cs @@ -0,0 +1,144 @@ +using System; +using System.Threading.Tasks; +using System.Linq; +using System.Text; + +using Discord; +using Discord.Commands; + +using Octokit; + +namespace Vita3KBot.Commands { + public class Compatibility: ModuleBase { + // Config + private const int MaxItemsToDisplay = 8; + + private const string HomebrewRepo = "homebrew-compatibility"; + private const string CommercialRepo = "compatibility"; + + private class TitleInfo { + private static readonly string[] StatusNames = { + // Priority, display when possible. + "Playable", + "Ingame", + "Intro", + "Crash", + "Nothing", + + // Secondary, display if nothing else. + "Slow", + "Black Screen", + "NID Missing", + "Module Loading Bug", + "IO Bug", + "Softlock Bug", + "Graphics Bug", + "Shader Bug", + "Audio Bug", + "Input Bug", + "Touch Bug", + "Savedata Bug", + "Trophy Bug", + "Networking Bug", + + // Invalid + "Invalid", + "Unknown", + }; + + private readonly Issue _issue; + public readonly bool IsHomebrew; + public readonly string Status; + public string LatestComment; + public string LatestProfileImage; + + public async Task FetchCommentInfo(GitHubClient client) { + if (_issue.Comments == 0) return; + + var comments = await client.Issue.Comment.GetAllForIssue("Vita3K", + IsHomebrew ? HomebrewRepo : CommercialRepo, _issue.Number); + var lastComment = comments[_issue.Comments - 1]; + LatestComment = "**" + lastComment.User.Login + "**: " + lastComment.Body; + LatestProfileImage = lastComment.User.AvatarUrl; + } + + public TitleInfo(Issue issue) { + _issue = issue; + // Repository object is sometimes null on searches. Just guess the repo by the URL. + IsHomebrew = issue.Url.Contains(HomebrewRepo); + Console.WriteLine(issue.CommentsUrl + " " + issue.Comments); + Status = "Unknown"; + + var foundStatus = false; + foreach (var label in issue.Labels) { + foreach (var name in StatusNames) { + if (name.ToLower().Equals(label.Name.ToLower())) { + Status = name; + foundStatus = true; + break; + } + } + if (foundStatus) break; + } + + LatestComment = "*No updates on this title.*"; + LatestProfileImage = ""; + } + } + + [Command("compat")] + public async Task Compatability([Remainder]string keyword) { + var github = new GitHubClient(new ProductHeaderValue("Vita3KBot")); + + var search = new SearchIssuesRequest(keyword) { + Repos = new RepositoryCollection { + "Vita3K/homebrew-compatibility", + "Vita3K/compatibility" + } + }; + + var result = await github.Search.SearchIssues(search); + switch (result.Items.Count) { + case 0: + await ReplyAsync("No games found for search term " + keyword + "."); + break; + + case 1: { + var issue = result.Items.First(); + var info = new TitleInfo(issue); + await info.FetchCommentInfo(github); + var builder = new EmbedBuilder() + .WithTitle("*" + issue.Title + "* (" + (info.IsHomebrew ? "Homebrew" : "Commercial") + ")") + .WithDescription("Status: **" + info.Status + "**\n\n" + info.LatestComment) + .WithColor(Color.Red) + .WithUrl(issue.Url) + .WithCurrentTimestamp(); + if (info.LatestProfileImage.Length > 0) builder.WithImageUrl(info.LatestProfileImage); + + await ReplyAsync("", false, builder.Build()); + break; + } + + default: { + var description = new StringBuilder(); + for (var a = 0; a < Math.Min(result.Items.Count, MaxItemsToDisplay); a++) { + var issue = result.Items[a]; + var info = new TitleInfo(issue); + description.Append("*" + issue.Title + "* (" + (info.IsHomebrew ? "Homebrew" : "Commercial") + + "): **" + info.Status + "**\n"); + } + if (result.Items.Count > MaxItemsToDisplay) description.Append("..."); + + var builder = new EmbedBuilder() + .WithTitle("Found " + result.Items.Count + " issues for search term " + keyword + ".") + .WithDescription(description.ToString()) + .WithColor(Color.Orange) + .WithCurrentTimestamp(); + + await ReplyAsync("", false, builder.Build()); + break; + } + } + } + } +} \ No newline at end of file diff --git a/Vita3KBot/Commands/Debug.cs b/Vita3KBot/Commands/Debug.cs new file mode 100644 index 0000000..0ed481a --- /dev/null +++ b/Vita3KBot/Commands/Debug.cs @@ -0,0 +1,17 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +using Discord.Commands; +using Discord.WebSocket; + +namespace Vita3KBot.Commands { + public class Debug : ModuleBase { + + // Get id for a role. Helpful when creating commands that might query, give, remove roles. + [Command("probe-role")] + public async Task ProbeRole([Remainder] string roleName) { + await ReplyAsync(roleName + ": " + Context.Guild.Roles.First(x => x.Name == roleName).Id); + } + } +} \ No newline at end of file diff --git a/Vita3KBot/MessageHandler.cs b/Vita3KBot/MessageHandler.cs new file mode 100644 index 0000000..81c3b56 --- /dev/null +++ b/Vita3KBot/MessageHandler.cs @@ -0,0 +1,81 @@ +using System; +using System.Data; +using System.Reflection; +using System.Threading.Tasks; + +using Discord; +using Discord.Commands; +using Discord.WebSocket; + +using Microsoft.Extensions.DependencyInjection; + +namespace Vita3KBot { + public class MessageHandler { + // Config + private const char Prefix = '-'; + private const bool ShowStackTrace = true; + + private readonly DiscordSocketClient _client; + + private readonly CommandService _commands; + private readonly ServiceProvider _services; + + // Called for each user message. Use it to collect stats, or silently observe stuff, etc. + private static async Task MonitorMessage(SocketUserMessage message) { + if (!(message.Author is SocketGuildUser user) || message.Author.IsBot) return; + + //TODO: Put Persona 4 Golden monitoring here. + } + + // Called by Discord.Net when it wants to log something. + private static Task Log(LogMessage message) { + Console.WriteLine(message.Message); + return Task.CompletedTask; + } + + // Called by Discord.Net when the bot receives a message. + private async Task CheckMessage(SocketMessage message) { + if (!(message is SocketUserMessage userMessage)) return; + + await MonitorMessage(userMessage); + + var prefixStart = 0; + + if (userMessage.HasCharPrefix(Prefix, ref prefixStart)) { + // Create Context and Execute Commands + var context = new SocketCommandContext(_client, userMessage); + var result = await _commands.ExecuteAsync(context, prefixStart, _services); + + // Handle any errors. + if (!result.IsSuccess && result.Error != CommandError.UnknownCommand) { + if (ShowStackTrace && result.Error == CommandError.Exception + && result is ExecuteResult execution) { + await userMessage.Channel.SendMessageAsync( + Utils.Code(execution.Exception.Message + "\n\n" + execution.Exception.StackTrace)); + } else { + await userMessage.Channel.SendMessageAsync( + "Halt! We've hit an error." + Utils.Code(result.ErrorReason)); + } + } + } + } + + // Initializes the Message Handler, subscribe to events, etc. + public async Task Init() { + _client.Log += Log; + _client.MessageReceived += CheckMessage; + + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + } + + public MessageHandler(DiscordSocketClient client) { + _client = client; + + _commands = new CommandService(); + _services = new ServiceCollection() + .AddSingleton(_client) + .AddSingleton(_commands) + .BuildServiceProvider(); + } + } +} \ No newline at end of file diff --git a/Vita3KBot/Utils.cs b/Vita3KBot/Utils.cs new file mode 100644 index 0000000..bb8a8db --- /dev/null +++ b/Vita3KBot/Utils.cs @@ -0,0 +1,31 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Vita3KBot { + public static class Utils { + + // Sends an HTTP Get request. Might not be the best solution. + public static async Task HttpGet(string address, string parameters = "") { + var client = new HttpClient{ BaseAddress = new Uri(address) }; + // Add User-Agent in header so Github API allows our requests. + client.DefaultRequestHeaders.Add("User-Agent", "Vita3KBot"); + var response = await client.GetAsync(parameters); + + if (response.IsSuccessStatusCode) { + var result = await response.Content.ReadAsStringAsync(); + client.Dispose(); + return result; + } + + client.Dispose(); + throw new Exception("Received " + response.StatusCode + + " status code from " + address + parameters + "."); + } + + // Discord Markdown Code. + public static string Code(string code, string lang = "") { + return "```" + lang + "\n" + code + "\n```"; + } + } +} \ No newline at end of file diff --git a/Vita3KBot/Vita3KBot.csproj b/Vita3KBot/Vita3KBot.csproj new file mode 100644 index 0000000..c8afead --- /dev/null +++ b/Vita3KBot/Vita3KBot.csproj @@ -0,0 +1,14 @@ + + + + Exe + netcoreapp2.1 + + + + + + + + +