public static string RetrieveFromKeychain(string Filename) { // Get the first line so we miss any formatting try { string RawKey = System.IO.File.ReadAllLines( System.IO.Path.Combine(Utils.BaseFilepath, "keychain", Filename) )[0]; // Clean token string Key = RawKey.Replace("\n", ""); return(Key); } catch (System.IO.FileNotFoundException) { if (Filename == "Token.txt") { // Sorry, but the script is totally useless without a token AuxillaryLogger.Log(new LogMessage(LogSeverity.Critical, "Login", "Failed to retrieve bot token from token.txt in keychain folder. " + "The script must now exit. Refer to the README to see how to set up " + "a bot and get its user account token.")); Utils.Exit(); } else { // The other API keys are somewhat optional AuxillaryLogger.Log(new LogMessage(LogSeverity.Warning, "Keychain retrieval", $"Failed to retrieve token/API key '{Filename}'. " + "Some functions of the bot may be unavailable as a result.")); } return(null); } catch (Exception e) { // If don't know what happened, then let the user know. // Some exceptions which aren't covered here should be obvious to any user // e.g. UnauthorizedAccessException/SecurityException AuxillaryLogger.Log(new LogMessage(LogSeverity.Error, "Keychain retrieval", $"In attempting to retrieve data from the keychain, an unknown error occured.\n" + $"Error details are as follows: \"{e.ToString()} - {e.Message}\"")); return(null); } }
// The actual code which connects our name determinant function to renaming the channel. // I didn't want to make the function which determines the name async, as it's more of a utility, // so all of the async work is done here. public static async Task UpdateVoiceChannel(IVoiceChannel Channel) { // If the setting is off - don't bother if (!Settings.GetGuildSetting <bool>(Channel.Guild, "RenameVoiceChannels")) { return; } // Don't mess with the AFK channel. Nobody plays games in it anyway...hence AFK if (Channel.Id == Channel.Guild.AFKChannelId) { return; } string NewName = GetVoiceChannelName(await Channel.GetUsersAsync().Flatten()); try { await Channel.ModifyAsync(x => x.Name = NewName); } catch (Exception e) { // If my checks haven't caught someone trying to (most likely) change the voice channel name to something // that it shouldn't be, then we can handle that to avoid crashing the bot AuxillaryLogger.Log(LogSeverity.Error, "VoiceChannelRename", $"Failed to change the voice channel's name to: {NewName} ({e.ToString()}: {e.Message})"); } }
public async Task Google([Remainder] string Query) { // First and foremost check that the command is enabled: if (!Settings.GetGuildSetting <bool>(Context.Guild, "AllowExtraCommands")) { return; } // Start time. Later in the embed I want to display search time elapsed, // this is the first step of being able to do that. var SearchStart = DateTime.UtcNow; /* Google's CSE API is utilized by querying the following URL: * https://www.googleapis.com/customsearch/v1?parameters * You can include such paramaters as (notably) query, API key, custom engine to use, * count, data filters and (not as notably) heuristics to provide personalized search * results. We won't be using those as this command is going to be used by all sorts * of people, in theory. */ if (String.IsNullOrEmpty(Keychain.GoogleAPIKey)) { AuxillaryLogger.Log(new LogMessage(LogSeverity.Error, "ExtraCommands.cs", "A Google query was requested but no API key is present in Clu/keychain/searchkey.txt." + " Aborting...")); return; } // Probably the first thing we want to do is anchor in a message that we'll edit later with the embed. // Imagine a scenario where a user requests a search and somehow this function hangs for 10 minutes, // perhaps due to an API outage, bad connection, some other Web mystery... // then once everyone has forgotten about it, the bot posts a random message and nobody has any idea why var EmbedMessage = await ReplyAsync("Searching..."); // Now, we should construct the URL: string SearchURL = "https://www.googleapis.com/customsearch/v1" + $"?q={Query}" + // Our query "&cx=011947631902407852034:gq02yx0e1mq" + // cx: A custom search engine I made that searches the whole web, to reduce setup // Don't think you can get in trouble for spam on this, unlike API keys, // so I've left mine in here. If I find out that you can, I'll have to add // some instructions for making your own in the README in keychain/ "&prettyPrint=false" + // prettyPrint: Disable indendations/line breaks for increased performance "&fields=items(title,snippet,link)" + // fields: Filter out everything except the results' titles and descriptions. // Increases performance. Currently hardcoded. May change later. $"&num=3" + // num: Limit the number of results to how many the user wants // Defaults to three. Should increase performance. $"&key={Keychain.GoogleAPIKey ?? String.Empty}"; // key: And finally, add our API key. It can be null, so I've put this here for safety, // although the above if statement should prevent a search query from getting // this far without any API key. // Prepare ourselves to decompress the gzip which we will request // Requesting gzip moves a considerable amount of work from the network // and then gives it to the CPU. Since network is usually the bottleneck here, // there should be substantial performance benefits. HttpClientHandler Handler = new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }; string StringResponse = String.Empty; using (var HC = new HttpClient(Handler)) { // Tell Google to send us gzip HC.DefaultRequestHeaders.Add("User-Agent", "CluSearch (gzip)"); HC.DefaultRequestHeaders.Add("Accept-Encoding", "gzip"); // Pre-declare so we can use outside of try-catch later var Response = await HC.GetAsync(new Uri(SearchURL)); // Try and read it to a string try { // Ensure everything went OK (in try block for obvious reasons) // Throws HttpRequestException for abnormal status codes Response.EnsureSuccessStatusCode(); // Get the string content of our response StringResponse = await Response.Content.ReadAsStringAsync(); } catch (HttpRequestException) { // With a proper web exception we can provide a more appropriate response AuxillaryLogger.Log( new LogMessage(LogSeverity.Error, "ExtraCommands.cs", "The HTTP request to Google's CSE API failed with" + $"status code {Response.StatusCode.ToString()}.")); throw; // Apparently this maintains the call stack } catch (Exception e) { // Else, if we have no idea what happened, just print a generic message AuxillaryLogger.Log( new LogMessage(LogSeverity.Error, "ExtraCommands.cs", "An unknown error occured in querying the Google CSE API.\n" + $"{e.Message}: {e.ToString()} (status: {Response.StatusCode.ToString()}")); throw; } } var JSON = JObject.Parse(StringResponse); // Serialize it into our Result class which has fields Title and Snippet List <Result> Results = JsonConvert.DeserializeObject <List <Result> >( JSON["items"].ToString() ); // Get our elapsed time TimeSpan SearchTimeTaken = DateTime.UtcNow.Subtract(SearchStart); AuxillaryLogger.Log(new LogMessage(LogSeverity.Debug, "ExtraCommands.cs", "Making embed...")); // Generate our embed // See https://cdn.discordapp.com/attachments/84319995256905728/252292324967710721/embed.png // for what all the fields are and how they end up looking var Embed = new EmbedBuilder() .WithAuthor(new EmbedAuthorBuilder() // Set the little icon in the top left to be the Google logo .WithIconUrl("http://i.imgur.com/EgnqiMG.png") .WithName($"\"{Query}\"")) // You need a name for the icon to show up :/ // Add time stats at the bottom .WithFooter(new EmbedFooterBuilder() .WithText($"Search completed in {SearchTimeTaken.Milliseconds}ms.")) // Set the URL that our title will point to as the actual Google results page // for the given query, should the user wish to know more. // Points to my CSE so as to ensure results are (for the most part) the same. .WithUrl("https://cse.google.co.uk/cse/" + "publicurl?cx=011947631902407852034:gq02yx0e1mq" + $"&q={Query.Replace(" ", "%20")}") // We must use %20 rather than spaces, otherwise the embed's URL is considered // to be invalid by Discord, and a BadRequest error will result if we try // and push it through. .WithColor(RandomColor); foreach (Result r in Results) { Embed.AddField( // Add our results to the embed r.Title, $"({URLPreview(r.Link)}) " + r.Snippet.Replace("\n", "") // And get rid of the newlines in snippets...they're everywhere...seeing one word lines :( // We have wrap anyway // Also have a URL preview to give the results a bit of context ); } // Finally, edit our message w/ embed: await EmbedMessage.ModifyAsync(m => { m.Content = string.Empty; m.Embed = Embed.Build(); }); }
public static async Task InitializeGuild(SocketGuild guild, IUser botUser) { AuxillaryLogger.Log(LogSeverity.Info, "Settings", $"Initializing settings for guild {guild.Name}..."); var StartTime = DateTime.UtcNow; // Helper function to setup guild; didn't want to make LoadSettings() async so it's not called from there MakeSettingsInstance(guild); var SettingsChannel = (ITextChannel)guild.TextChannels.Where(c => c.Name == "clu-bot-settings").FirstOrDefault(); if (SettingsChannel == null) { SettingsChannel = (ITextChannel)await guild.CreateTextChannelAsync("clu-bot-settings"); await SettingsChannel.AddPermissionOverwriteAsync( guild.EveryoneRole, OverwritePermissions.DenyAll(SettingsChannel) ); } // Don't await getting the message every time in the function call in the loop, get it once and for all var Messages = await SettingsChannel.GetMessagesAsync().Flatten(); // Also, we can clean out the channel here foreach (IMessage m in Messages) { if (m.Author.Id != botUser.Id) { await m.DeleteAsync(); await Task.Delay(2000); } } foreach (DeserializedBotSetting Setting in _BaseSettings) { var PossiblyExistingMessage = (IUserMessage)SettingInChannel(Setting, Messages); if (PossiblyExistingMessage == null) { // Performance diagnostics and that AuxillaryLogger.Log(LogSeverity.Verbose, "Settings", "\tEncountered missing settings message, posting... (+6 seconds)"); var SettingsMessage = await SettingsChannel.SendMessageAsync( Setting.Description + $" (default: {Setting.DefaultValueStr})" ); // I've introduced sleep statements at various places to keep the rate-limits at bay... // as it's a setup function, speed isn't of the essence. await Task.Delay(2000); // RestMessage inherits from IUserMessage, so we can now get to a real type // No method for this as the resultant type will vary... switch (Setting.ValueType) { case ValueData.Bool: // Add relevant reactions await SettingsMessage.AddReactionAsync(new Emoji("✔")); await Task.Delay(2000); await SettingsMessage.AddReactionAsync(new Emoji("❌")); await Task.Delay(2000); // The constructor will add it to the relevant lists. No need to assign. // In addition, the guild isn't passed because it's already implied by the message new GuildBotSettingYN(Setting, SettingsMessage); break; case ValueData.Role: case ValueData.Roles: case ValueData.User: case ValueData.Users: throw new NotImplementedException(); default: throw new ArgumentException($"Could not determine what type a message with ValueDataString {Setting.ValueTypeString} should be remade into!"); } } else { // Here's why it returns an IMessage, not a bool // If already found, but no reactions for some reason, add them var ReactionDataTick = await PossiblyExistingMessage.GetReactionUsersAsync("✔"); if (!ReactionDataTick.Any(u => u.Id == botUser.Id)) { AuxillaryLogger.Log(LogSeverity.Verbose, "Settings", "\tEncountered missing bot reaction, adding... (+2 seconds)"); await PossiblyExistingMessage.AddReactionAsync(new Emoji("✔")); await Task.Delay(2000); } var ReactionDataCross = await PossiblyExistingMessage.GetReactionUsersAsync("❌"); if (!ReactionDataCross.Any(u => u.Id == botUser.Id)) { AuxillaryLogger.Log(LogSeverity.Verbose, "Settings", "\tEncountered missing bot reaction, adding... (+2 seconds)"); await PossiblyExistingMessage.AddReactionAsync(new Emoji("❌")); await Task.Delay(2000); } // Once corrected, create setting objects switch (Setting.ValueType) { case ValueData.Bool: new GuildBotSettingYN(Setting, PossiblyExistingMessage); break; case ValueData.Role: case ValueData.Roles: case ValueData.User: case ValueData.Users: throw new NotImplementedException(); default: throw new ArgumentException($"Could not determine what type a message with ValueDataString {Setting.ValueTypeString} should be remade into!"); } } } double TimeDelta = Math.Round((DateTime.UtcNow - StartTime).TotalSeconds, 3); AuxillaryLogger.Log(LogSeverity.Info, "Settings", $"Completed settings initialization for guild {guild.Name} in {TimeDelta} seconds." ); }