diff --git a/.gitignore b/.gitignore index a90f989f5..c4588d4f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,14 @@ #Manually added files -patreon_rewards.json command_errors*.txt _output/ src/NadekoBot/Command Errors*.txt -src/NadekoBot/credentials.json -# these 2 are used for migrations -NadekoBot.Core/credentials.json -NadekoBot.Core/credentials_example.json - +src/NadekoBot/creds.yml +# credentials file after migrations +src/NadekoBot/old_credentials.json src/NadekoBot/data/NadekoBot.db -src/NadekoBot/data/musicdata # Created by https://www.gitignore.io/api/visualstudio,visualstudiocode,windows,linux,macos diff --git a/src/NadekoBot/Bot.cs b/src/NadekoBot/Bot.cs index 9f5617c0d..2e2ae1f8a 100644 --- a/src/NadekoBot/Bot.cs +++ b/src/NadekoBot/Bot.cs @@ -12,7 +12,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; @@ -43,26 +42,23 @@ namespace NadekoBot public static Color ErrorColor { get; set; } public static Color PendingColor { get; set; } + // todo remove ready prop public TaskCompletionSource Ready { get; private set; } = new TaskCompletionSource(); public IServiceProvider Services { get; private set; } - public IDataCache Cache { get; private set; } - + public string Mention { get; set; } public event Func JoinedGuild = delegate { return Task.CompletedTask; }; + private readonly BotCredsProvider _credsProvider; public Bot(int shardId) { if (shardId < 0) throw new ArgumentOutOfRangeException(nameof(shardId)); - - TerribleElevatedPermissionCheck(); - _creds = BotCredentialsProvider.CreateBotCredentials(); - - // todo no need for cache prop - Cache = new RedisCache(_creds, shardId); + _credsProvider = new BotCredsProvider(); + _creds = _credsProvider.GetCreds(); _db = new DbService(_creds); @@ -115,21 +111,21 @@ namespace NadekoBot uow.EnsureUserCreated(_bot.Id, _bot.Username, _bot.Discriminator, _bot.AvatarId); AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(startingGuildIdList).ToImmutableArray(); } - + var svcs = new ServiceCollection() - .AddSingleton(_creds) - .AddSingleton(_db) - .AddSingleton(Client) + .AddTransient(_ => _creds) // bot creds + .AddSingleton(_db) // database + .AddRedis(_creds.RedisOptions) // redis + .AddSingleton(Client) // discord socket client .AddSingleton(CommandService) - .AddSingleton(this) - .AddSingleton(Cache) - .AddSingleton(Cache.Redis) + .AddSingleton(this) // pepega + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddBotStringsServices() .AddConfigServices() - .AddConfigMigrators() + .AddConfigMigrators() // todo remove config migrators .AddMemoryCache() .AddSingleton() // music @@ -175,6 +171,7 @@ namespace NadekoBot Log.Information($"All services loaded in {sw.Elapsed.TotalSeconds:F2}s"); } + // todo remove config migrations private void ApplyConfigMigrations() { // execute all migrators @@ -192,6 +189,7 @@ namespace NadekoBot // deleteBotConfig.ExecuteNonQuery(); } + // todo isn't there a built in for loading type readers? private IEnumerable LoadTypeReaders(Assembly assembly) { Type[] allTypes; @@ -271,8 +269,10 @@ namespace NadekoBot Client.Ready += SetClientReady; await clientReady.Task.ConfigureAwait(false); Client.Ready -= SetClientReady; + Client.JoinedGuild += Client_JoinedGuild; Client.LeftGuild += Client_LeftGuild; + Log.Information("Shard {0} logged in.", Client.ShardId); } @@ -297,6 +297,7 @@ namespace NadekoBot return Task.CompletedTask; } + // todo cleanup public async Task RunAsync() { var sw = Stopwatch.StartNew(); @@ -346,8 +347,10 @@ namespace NadekoBot } catch (Exception ex) { - Log.Error(ex, "Failed running OnReadyAsync method on {Type} type: {Message}", - toExec.GetType().Name, ex.Message); + Log.Error(ex, + "Failed running OnReadyAsync method on {Type} type: {Message}", + toExec.GetType().Name, + ex.Message); } }); @@ -370,22 +373,8 @@ namespace NadekoBot await Task.Delay(-1).ConfigureAwait(false); } - private void TerribleElevatedPermissionCheck() - { - try - { - var rng = new NadekoRandom().Next(100000, 1000000); - var str = rng.ToString(); - File.WriteAllText(str, str); - File.Delete(str); - } - catch - { - Log.Error("You must run the application as an ADMINISTRATOR"); - Helpers.ReadErrorAndExit(2); - } - } - + + // todo status changes don't belong here private void HandleStatusChanges() { var sub = Services.GetService().Redis.GetSubscriber(); diff --git a/src/NadekoBot/Common/Creds.cs b/src/NadekoBot/Common/Creds.cs index 3a63f9e7c..8f9b949ca 100644 --- a/src/NadekoBot/Common/Creds.cs +++ b/src/NadekoBot/Common/Creds.cs @@ -9,8 +9,9 @@ namespace Nadeko.Common { public Creds() { + Version = 1; Token = string.Empty; - OwnerIds = new(); + OwnerIds = new List(); TotalShards = 1; GoogleApiKey = string.Empty; Votes = new(string.Empty, string.Empty); @@ -23,15 +24,17 @@ namespace Nadeko.Common Type = "sqlite", ConnectionString = "Data Source=data/NadekoBot.db" }; - Version = 1; } + [Comment(@"DO NOT CHANGE")] + public int Version { get; set; } + [Comment(@"Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/")] public string Token { get; set; } [Comment(@"List of Ids of the users who have bot owner permissions **DO NOT ADD PEOPLE YOU DON'T TRUST**")] - public List OwnerIds { get; set; } + public ICollection OwnerIds { get; set; } // todo update total shards on startup [Comment(@"The number of shards that the bot will running on. @@ -61,9 +64,6 @@ go to https://www.patreon.com/portal -> my clients -> create client")] [Comment(@"Database options. Don't change if you don't know what you're doing. Leave null for default values")] public DbOptions Db { get; set; } - - [Comment(@"DO NOT CHANGE")] - public int Version { get; set; } public RestartConfig RestartCommand { get; set; } @@ -153,7 +153,7 @@ Used for cryptocurrency related commands.")] public int TotalShards { get; set; } = 1; public string PatreonAccessToken { get; set; } = string.Empty; public string PatreonCampaignId { get; set; } = "334038"; - public RestartConfig? RestartCommand { get; set; } = null; + public RestartConfig RestartCommand { get; set; } = null; public string ShardRunCommand { get; set; } = string.Empty; public string ShardRunArguments { get; set; } = string.Empty; diff --git a/src/NadekoBot/Common/Extensions/ServiceCollectionExtensions.cs b/src/NadekoBot/Common/Extensions/ServiceCollectionExtensions.cs index 6bc877d19..4a41c4c8a 100644 --- a/src/NadekoBot/Common/Extensions/ServiceCollectionExtensions.cs +++ b/src/NadekoBot/Common/Extensions/ServiceCollectionExtensions.cs @@ -8,6 +8,7 @@ using NadekoBot.Services; using NadekoBot.Modules.Administration.Services; using NadekoBot.Modules.Music.Resolvers; using NadekoBot.Modules.Music.Services; +using StackExchange.Redis; namespace NadekoBot.Extensions { @@ -65,5 +66,12 @@ namespace NadekoBot.Extensions return services; } + + public static IServiceCollection AddRedis(this IServiceCollection services, string redisOptions) + { + var conf = ConfigurationOptions.Parse(redisOptions); + services.AddSingleton(ConnectionMultiplexer.Connect(conf)); + return services; + } } } \ No newline at end of file diff --git a/src/NadekoBot/Db/NadekoContext.cs b/src/NadekoBot/Db/NadekoContext.cs index be6d644a0..dc943cf9c 100644 --- a/src/NadekoBot/Db/NadekoContext.cs +++ b/src/NadekoBot/Db/NadekoContext.cs @@ -17,7 +17,7 @@ namespace NadekoBot.Services.Database { LogSetup.SetupLogger(-2); var optionsBuilder = new DbContextOptionsBuilder(); - IBotCredentials creds = BotCredentialsProvider.CreateBotCredentials(); + IBotCredentials creds = new BotCredsProvider().GetCreds(); var builder = new SqliteConnectionStringBuilder(creds.Db.ConnectionString); builder.DataSource = Path.Combine(AppContext.BaseDirectory, builder.DataSource); optionsBuilder.UseSqlite(builder.ToString()); diff --git a/src/NadekoBot/Modules/Administration/Services/SelfService.cs b/src/NadekoBot/Modules/Administration/Services/SelfService.cs index 613d85100..a506518b0 100644 --- a/src/NadekoBot/Modules/Administration/Services/SelfService.cs +++ b/src/NadekoBot/Modules/Administration/Services/SelfService.cs @@ -38,11 +38,10 @@ namespace NadekoBot.Modules.Administration.Services private readonly IImageCache _imgs; private readonly IHttpClientFactory _httpFactory; private readonly BotConfigService _bss; - private readonly ICoordinator _coord; public SelfService(DiscordSocketClient client, CommandHandler cmdHandler, DbService db, IBotStrings strings, IBotCredentials creds, IDataCache cache, IHttpClientFactory factory, - BotConfigService bss, ICoordinator coord) + BotConfigService bss) { _redis = cache.Redis; _cmdHandler = cmdHandler; @@ -54,7 +53,6 @@ namespace NadekoBot.Modules.Administration.Services _imgs = cache.LocalImages; _httpFactory = factory; _bss = bss; - _coord = coord; var sub = _redis.GetSubscriber(); if (_client.ShardId == 0) diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 90f0c666e..bafd5dcc9 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -51,17 +51,12 @@ + - - - - Always - - @@ -73,13 +68,7 @@ PreserveNewest - - Always - - - Always - - + Always diff --git a/src/NadekoBot/Services/IBotCredentials.cs b/src/NadekoBot/Services/IBotCredentials.cs index c8370d65f..33472aab2 100644 --- a/src/NadekoBot/Services/IBotCredentials.cs +++ b/src/NadekoBot/Services/IBotCredentials.cs @@ -10,7 +10,7 @@ namespace NadekoBot.Services { string Token { get; } string GoogleApiKey { get; } - List OwnerIds { get; } + ICollection OwnerIds { get; } string RapidApiKey { get; } string PatreonAccessToken { get; } diff --git a/src/NadekoBot/Services/Impl/BotCredentials.cs b/src/NadekoBot/Services/Impl/BotCredentials.cs index e74d3416b..72d2d1e61 100644 --- a/src/NadekoBot/Services/Impl/BotCredentials.cs +++ b/src/NadekoBot/Services/Impl/BotCredentials.cs @@ -1,119 +1,63 @@ using Microsoft.Extensions.Configuration; using System.IO; +using Microsoft.Extensions.Primitives; using Nadeko.Common; +using NadekoBot.Common.Yml; using Serilog; namespace NadekoBot.Services { - public static class BotCredentialsProvider + // todo check why is memory usage so unstable + public class BotCredsProvider { private const string _credsFileName = "creds.yml"; - private static string _oldCredsJsonFilename = Path.Combine(Directory.GetCurrentDirectory(), "credentials.json"); + private string CredsPath => Path.Combine(Directory.GetCurrentDirectory(), _credsFileName); + private const string _credsExampleFileName = "creds_example.yml"; + private string CredsExamplePath => Path.Combine(Directory.GetCurrentDirectory(), _credsExampleFileName); - public static Creds CreateBotCredentials() + private string _oldCredsJsonFilename = Path.Combine(Directory.GetCurrentDirectory(), "credentials.json"); + + private Creds _creds = new Creds(); + private IConfigurationRoot _config; + + private readonly object reloadLock = new object(); + private void Reload() { - if (!File.Exists(_credsFileName)) - Log.Warning($"{_credsFileName} is missing. " + - $"Attempting to load creds from environment variables prefixed with 'NadekoBot_'. " + - $"Example is in {Path.GetFullPath("./creds-example.yml")}"); - - - IConfigurationBuilder configBuilder = new ConfigurationBuilder(); - var creds = configBuilder - .AddYamlFile(_credsFileName, false, true) - .AddEnvironmentVariables("NadekoBot_") - .Build() - .Get(); - - // if(string.IsNullOrWhiteSpace(creds.RedisOptions)) - // creds.RedisOptions = "" - - return creds; - - // try - // { - // - // - // var data = configBuilder.Build(); - // - // Token = data[nameof(Token)]; - // if (string.IsNullOrWhiteSpace(Token)) - // { - // Log.Error("Token is missing from credentials.json or Environment variables. Add it and restart the program."); - // Helpers.ReadErrorAndExit(5); - // } - // - // OwnerIds = data.GetSection("OwnerIds").GetChildren().Select(c => ulong.Parse(c.Value)) - // .ToImmutableArray(); - // GoogleApiKey = data[nameof(GoogleApiKey)]; - // MashapeKey = data[nameof(MashapeKey)]; - // OsuApiKey = data[nameof(OsuApiKey)]; - // PatreonAccessToken = data[nameof(PatreonAccessToken)]; - // PatreonCampaignId = data[nameof(PatreonCampaignId)] ?? "334038"; - // ShardRunCommand = data[nameof(ShardRunCommand)]; - // ShardRunArguments = data[nameof(ShardRunArguments)]; - // CleverbotApiKey = data[nameof(CleverbotApiKey)]; - // LocationIqApiKey = data[nameof(LocationIqApiKey)]; - // TimezoneDbApiKey = data[nameof(TimezoneDbApiKey)]; - // CoinmarketcapApiKey = data[nameof(CoinmarketcapApiKey)]; - // if (string.IsNullOrWhiteSpace(CoinmarketcapApiKey)) - // { - // CoinmarketcapApiKey = "e79ec505-0913-439d-ae07-069e296a6079"; - // } - // - // if (!string.IsNullOrWhiteSpace(data[nameof(RedisOptions)])) - // RedisOptions = data[nameof(RedisOptions)]; - // else - // RedisOptions = "127.0.0.1,syncTimeout=3000"; - // - // VotesToken = data[nameof(VotesToken)]; - // VotesUrl = data[nameof(VotesUrl)]; - // BotListToken = data[nameof(BotListToken)]; - // - // var restartSection = data.GetSection(nameof(RestartCommand)); - // var cmd = restartSection["cmd"]; - // var args = restartSection["args"]; - // if (!string.IsNullOrWhiteSpace(cmd)) - // RestartCommand = new RestartConfig(cmd, args); - // - // if (Environment.OSVersion.Platform == PlatformID.Unix) - // { - // if (string.IsNullOrWhiteSpace(ShardRunCommand)) - // ShardRunCommand = "dotnet"; - // if (string.IsNullOrWhiteSpace(ShardRunArguments)) - // ShardRunArguments = "run -c Release --no-build -- {0} {1}"; - // } - // else //windows - // { - // if (string.IsNullOrWhiteSpace(ShardRunCommand)) - // ShardRunCommand = "NadekoBot.exe"; - // if (string.IsNullOrWhiteSpace(ShardRunArguments)) - // ShardRunArguments = "{0} {1}"; - // } - // - // if (!int.TryParse(data[nameof(TotalShards)], out var ts)) - // ts = 0; - // TotalShards = ts < 1 ? 1 : ts; - // - // CarbonKey = data[nameof(CarbonKey)]; - // var dbSection = data.GetSection("db"); - // Db = new DBConfig(@"sqlite", - // string.IsNullOrWhiteSpace(dbSection["ConnectionString"]) - // ? "Data Source=data/NadekoBot.db" - // : dbSection["ConnectionString"]); - // - // TwitchClientId = data[nameof(TwitchClientId)]; - // if (string.IsNullOrWhiteSpace(TwitchClientId)) - // { - // TwitchClientId = "67w6z9i09xv2uoojdm9l0wsyph4hxo6"; - // } - // } - // catch (Exception ex) - // { - // Log.Error("JSON serialization has failed. Fix your credentials file and restart the bot."); - // Log.Fatal(ex.ToString()); - // Helpers.ReadErrorAndExit(6); - // } + lock (reloadLock) + { + _creds.OwnerIds.Clear(); + _config.Bind(_creds); + + // todo load defaults for restart command, redis, and some others maybe? + } } + + public BotCredsProvider() + { + if (!File.Exists(CredsExamplePath)) + { + File.WriteAllText(CredsExamplePath, Yaml.Serializer.Serialize(_creds)); + } + + if (!File.Exists(CredsPath)) + { + Log.Warning($"{CredsPath} is missing. " + + $"Attempting to load creds from environment variables prefixed with 'NadekoBot_'. " + + $"Example is in {CredsExamplePath}"); + } + + _config = new ConfigurationBuilder() + .AddYamlFile(CredsPath, false, true) + .AddEnvironmentVariables("NadekoBot_") + .Build(); + + ChangeToken.OnChange( + () => _config.GetReloadToken(), + Reload); + + Reload(); + } + + public Creds GetCreds() => _creds; } } \ No newline at end of file diff --git a/src/NadekoBot/Services/Impl/RedisCache.cs b/src/NadekoBot/Services/Impl/RedisCache.cs index 32706f175..90258d13f 100644 --- a/src/NadekoBot/Services/Impl/RedisCache.cs +++ b/src/NadekoBot/Services/Impl/RedisCache.cs @@ -5,6 +5,8 @@ using System; using System.Linq; using System.Net; using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; namespace NadekoBot.Services { @@ -18,14 +20,12 @@ namespace NadekoBot.Services private readonly string _redisKey; private readonly EndPoint _redisEndpoint; - public RedisCache(IBotCredentials creds, int shardId) + public RedisCache(ConnectionMultiplexer redis, IBotCredentials creds, DiscordSocketClient client) { - var conf = ConfigurationOptions.Parse(creds.RedisOptions); - - Redis = ConnectionMultiplexer.Connect(conf); + Redis = redis; _redisEndpoint = Redis.GetEndPoints().First(); LocalImages = new RedisImagesCache(Redis, creds); - LocalData = new RedisLocalDataCache(Redis, creds, shardId); + LocalData = new RedisLocalDataCache(Redis, creds, client.ShardId); _redisKey = creds.RedisKey(); } diff --git a/src/NadekoBot/credentials_example.json b/src/NadekoBot/credentials_example.json deleted file mode 100644 index 18849be99..000000000 --- a/src/NadekoBot/credentials_example.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "Token": "", - "OwnerIds": [ - 105635576866156544 - ], - "GoogleApiKey": "", - "MashapeKey": "", - "OsuApiKey": "", - "SoundCloudClientId": "", - "CleverbotApiKey": "", - "CarbonKey": "", - "Db": { - "Type": "sqlite", - "ConnectionString": "Data Source=data/NadekoBot.db" - }, - "TotalShards": 1, - "PatreonAccessToken": "", - "PatreonCampaignId": "334038", - "RestartCommand": null, - "ShardRunCommand": "", - "ShardRunArguments": "", - "ShardRunPort": null, - "BotListToken": null, - "TwitchClientId": null, - "VotesToken": null, - "VotesUrl": null, - "RedisOptions": null, - "LocationIqApiKey": null, - "TimezoneDbApiKey": null, - "CoinmarketcapApiKey": null -} \ No newline at end of file diff --git a/src/NadekoBot/creds.yml b/src/NadekoBot/creds.yml new file mode 100644 index 000000000..a0a29c6e5 --- /dev/null +++ b/src/NadekoBot/creds.yml @@ -0,0 +1,57 @@ +# DO NOT CHANGE +version: 1 +# Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/ +token: +# List of Ids of the users who have bot owner permissions +# **DO NOT ADD PEOPLE YOU DON'T TRUST** +ownerIds: +# The number of shards that the bot will running on. +# Leave at 1 if you don't know what you're doing. +totalShards: 1 +# Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it. +# Then, go to APIs and Services -> Credentials and click Create credentials -> API key. +# Used only for Youtube Data Api (at the moment). +googleApiKey: '' +# Settings for voting system for discordbots. Meant for use on global Nadeko. +votes: + url: '' + key: '' +# Patreon auto reward system settings. +# go to https://www.patreon.com/portal -> my clients -> create client +patreon: +# Access token. You have to manually update this 1st of each month by refreshing the token on https://patreon.com/portal + accessToken: '' + # Unused atm + refreshToken: '' + # Unused atm + clientSecret: '' + # Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type "prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);" in the console. (ctrl + shift + i) + campaignId: '' +# Api key for sending stats to DiscordBotList. +botListToken: '' +# Official cleverbot api key. +cleverbotApiKey: '' +# Redis connection string. Don't change if you don't know what you're doing. +redisOptions: localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password= +# Database options. Don't change if you don't know what you're doing. Leave null for default values +db: +# Database type. Only sqlite supported atm + type: sqlite + # Connection string. Will default to "Data Source=data/NadekoBot.db" + connectionString: Data Source=data/NadekoBot.db +restartCommand: +votesUrl: +votesToken: +# Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key) +rapidApiKey: +# https://locationiq.com api key (register and you will receive the token in the email). +# Used only for .time command. +locationIqApiKey: +# https://timezonedb.com api key (register and you will receive the token in the email). +# Used only for .time command +timezoneDbApiKey: +# https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use. +# Used for cryptocurrency related commands. +coinmarketcapApiKey: +# Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api +osuApiKey: diff --git a/src/NadekoBot/creds_example.yml b/src/NadekoBot/creds_example.yml new file mode 100644 index 000000000..f6f17c5e0 --- /dev/null +++ b/src/NadekoBot/creds_example.yml @@ -0,0 +1,57 @@ +# DO NOT CHANGE +version: 1 +# Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/ +token: '' +# List of Ids of the users who have bot owner permissions +# **DO NOT ADD PEOPLE YOU DON'T TRUST** +ownerIds: +# The number of shards that the bot will running on. +# Leave at 1 if you don't know what you're doing. +totalShards: 1 +# Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it. +# Then, go to APIs and Services -> Credentials and click Create credentials -> API key. +# Used only for Youtube Data Api (at the moment). +googleApiKey: '' +# Settings for voting system for discordbots. Meant for use on global Nadeko. +votes: + url: '' + key: '' +# Patreon auto reward system settings. +# go to https://www.patreon.com/portal -> my clients -> create client +patreon: +# Access token. You have to manually update this 1st of each month by refreshing the token on https://patreon.com/portal + accessToken: '' + # Unused atm + refreshToken: '' + # Unused atm + clientSecret: '' + # Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type "prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);" in the console. (ctrl + shift + i) + campaignId: '' +# Api key for sending stats to DiscordBotList. +botListToken: '' +# Official cleverbot api key. +cleverbotApiKey: '' +# Redis connection string. Don't change if you don't know what you're doing. +redisOptions: localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password= +# Database options. Don't change if you don't know what you're doing. Leave null for default values +db: +# Database type. Only sqlite supported atm + type: sqlite + # Connection string. Will default to "Data Source=data/NadekoBot.db" + connectionString: Data Source=data/NadekoBot.db +restartCommand: +votesUrl: +votesToken: +# Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key) +rapidApiKey: +# https://locationiq.com api key (register and you will receive the token in the email). +# Used only for .time command. +locationIqApiKey: +# https://timezonedb.com api key (register and you will receive the token in the email). +# Used only for .time command +timezoneDbApiKey: +# https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use. +# Used for cryptocurrency related commands. +coinmarketcapApiKey: +# Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api +osuApiKey: