From c1047c6ed22dc69a99644a30f3b85361a606bf66 Mon Sep 17 00:00:00 2001 From: Arcanox <816612+ArcanoxDragon@users.noreply.github.com> Date: Mon, 14 Dec 2020 14:29:17 -0600 Subject: [PATCH] Require geofence files to be specified in Discord server config instead of loading implicitly (#105) * Require Geofence files to be explicitly listed in each Discord server (under new "geofences" property, similar to alarms) instead of loading all files implicitly * Add method to retrieve all geofences for a particular server (for another contributor's PR later) * Add missing lock-blocks for access to _geofences cache * Make a couple example geofence files use a json extension to show off GeoJSON support * Allow alarms to specify geofence files that are not specified for any servers * To ensure only one instance of the WhConfig class is alive anywhere, use an object holder to keep track of that instance and allow it to be swapped out when the config is reloaded * Store geofences on DiscordServerConfig class and use them for area-accepting commands when EnableCities is false * Probably should just use lock() here * Fix a merge issue * Update SubscriptionManager.cs * Update Bot.cs Co-authored-by: versx Co-authored-by: versx --- config.example.json | 8 + src/Bot.cs | 82 ++++---- src/Commands/Dependencies.cs | 8 +- src/Commands/Input/SubscriptionInput.cs | 11 +- src/Commands/Nests.cs | 6 +- src/Commands/Notifications.cs | 47 +++-- src/Configuration/DiscordServerConfig.cs | 13 +- src/Configuration/WhConfigHolder.cs | 48 +++++ src/Data/Subscriptions/SubscriptionManager.cs | 16 +- .../Subscriptions/SubscriptionProcessor.cs | 197 ++++++++++++------ src/Geofence/GeofenceService.cs | 30 +-- src/Net/Webhooks/WebhookController.cs | 169 ++++++++++----- src/Program.cs | 6 +- test/GeofenceTest.cs | 32 ++- 14 files changed, 443 insertions(+), 230 deletions(-) create mode 100644 src/Configuration/WhConfigHolder.cs diff --git a/config.example.json b/config.example.json index 75a2e8d6..8229e3b8 100644 --- a/config.example.json +++ b/config.example.json @@ -13,6 +13,10 @@ "moderatorRoleIds": [], "token": "", "alarms": "alarms.json", + "geofences": [ + "City1.txt", + "City2.json" + ], "subscriptions": { "enabled": false, "maxPokemonSubscriptions": 0, @@ -94,6 +98,10 @@ "moderatorRoleIds": [], "token": "", "alarms": "alarms2.json", + "geofences": [ + "City3.txt", + "City4.json" + ], "subscriptions": { "enabled": false, "maxPokemonSubscriptions": 0, diff --git a/src/Bot.cs b/src/Bot.cs index a555b4f7..2ecac6e2 100644 --- a/src/Bot.cs +++ b/src/Bot.cs @@ -39,7 +39,7 @@ public class Bot private readonly Dictionary _servers; private readonly WebhookController _whm; - private WhConfig _whConfig; + private readonly WhConfigHolder _whConfig; private readonly SubscriptionProcessor _subProcessor; private static readonly IEventLogger _logger = EventLogger.GetLogger("BOT"); @@ -52,22 +52,22 @@ public class Bot /// Discord bot class /// /// Configuration settings - public Bot(WhConfig whConfig) + public Bot(WhConfigHolder whConfig) { - _logger.Trace($"WhConfig [Servers={whConfig.Servers.Count}, Port={whConfig.WebhookPort}]"); + _logger.Trace($"WhConfig [Servers={whConfig.Instance.Servers.Count}, Port={whConfig.Instance.WebhookPort}]"); _servers = new Dictionary(); _whConfig = whConfig; _whm = new WebhookController(_whConfig); // Build form lists for icons - IconFetcher.Instance.SetIconStyles(_whConfig.IconStyles); + IconFetcher.Instance.SetIconStyles(_whConfig.Instance.IconStyles); // Set translation language - Translator.Instance.SetLocale(_whConfig.Locale); + Translator.Instance.SetLocale(_whConfig.Instance.Locale); // Set database connection strings to static properties so we can access within our extension classes - DataAccessLayer.ConnectionString = _whConfig.Database.Main.ToString(); - DataAccessLayer.ScannerConnectionString = _whConfig.Database.Scanner.ToString(); + DataAccessLayer.ConnectionString = _whConfig.Instance.Database.Main.ToString(); + DataAccessLayer.ScannerConnectionString = _whConfig.Instance.Database.Scanner.ToString(); // Set unhandled exception event handler AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler; @@ -79,7 +79,7 @@ public Bot(WhConfig whConfig) // Initialize the subscription processor if at least one Discord server wants custom notifications // and start database migrator - if (_whConfig.Servers.Values.ToList().Exists(x => x.Subscriptions.Enabled)) + if (_whConfig.Instance.Servers.Values.ToList().Exists(x => x.Subscriptions.Enabled)) { // Start database migrator var migrator = new DatabaseMigrator(); @@ -92,7 +92,7 @@ public Bot(WhConfig whConfig) } // Create a DiscordClient object per Discord server in config - foreach (var (guildId, serverConfig) in _whConfig.Servers) + foreach (var (guildId, serverConfig) in _whConfig.Instance.Servers) { serverConfig.LoadDmAlerts(); var client = new DiscordClient(new DiscordConfiguration @@ -150,7 +150,7 @@ public Bot(WhConfig whConfig) DependencyCollection dep; using (var d = new DependencyCollectionBuilder()) { - d.AddInstance(new Dependencies(interactivity, _whm, _subProcessor, _whConfig, new StripeService(_whConfig.StripeApiKey))); + d.AddInstance(new Dependencies(interactivity, _whm, _subProcessor, _whConfig, new StripeService(_whConfig.Instance.StripeApiKey))); dep = d.Build(); } @@ -232,7 +232,7 @@ public async Task Start() _whm.GymDetailsAlarmTriggered += OnGymDetailsAlarmTriggered; _whm.WeatherAlarmTriggered += OnWeatherAlarmTriggered; // At least one server wants subscriptions - if (_whConfig.Servers.FirstOrDefault(x => x.Value.Subscriptions.Enabled).Value != null) + if (_whConfig.Instance.Servers.FirstOrDefault(x => x.Value.Subscriptions.Enabled).Value != null) { // Register subscription event handlers _whm.PokemonSubscriptionTriggered += OnPokemonSubscriptionTriggered; @@ -270,7 +270,7 @@ public async Task Stop() _whm.GymAlarmTriggered -= OnGymAlarmTriggered; _whm.GymDetailsAlarmTriggered -= OnGymDetailsAlarmTriggered; _whm.WeatherAlarmTriggered -= OnWeatherAlarmTriggered; - if (_whConfig.Servers.FirstOrDefault(x => x.Value.Subscriptions.Enabled).Value != null) + if (_whConfig.Instance.Servers.FirstOrDefault(x => x.Value.Subscriptions.Enabled).Value != null) { //At least one server wanted subscriptions, unregister the subscription event handlers _whm.PokemonSubscriptionTriggered -= OnPokemonSubscriptionTriggered; @@ -307,7 +307,7 @@ private Task Client_Ready(ReadyEventArgs e) private async Task Client_GuildAvailable(GuildCreateEventArgs e) { // If guild is in configured servers list then attempt to create emojis needed - if (_whConfig.Servers.ContainsKey(e.Guild.Id)) + if (_whConfig.Instance.Servers.ContainsKey(e.Guild.Id)) { // Create default emojis await CreateEmojis(e.Guild.Id); @@ -319,9 +319,9 @@ private async Task Client_GuildAvailable(GuildCreateEventArgs e) } // Set custom bot status if guild is in config server list - if (_whConfig.Servers.ContainsKey(e.Guild.Id)) + if (_whConfig.Instance.Servers.ContainsKey(e.Guild.Id)) { - var status = _whConfig.Servers[e.Guild.Id].Status; + var status = _whConfig.Instance.Servers[e.Guild.Id].Status; await client.UpdateStatusAsync(new DiscordGame(status ?? $"v{Strings.Version}"), UserStatus.Online); } } @@ -329,10 +329,10 @@ private async Task Client_GuildAvailable(GuildCreateEventArgs e) private async Task Client_GuildMemberUpdated(GuildMemberUpdateEventArgs e) { - if (!_whConfig.Servers.ContainsKey(e.Guild.Id)) + if (!_whConfig.Instance.Servers.ContainsKey(e.Guild.Id)) return; - var server = _whConfig.Servers[e.Guild.Id]; + var server = _whConfig.Instance.Servers[e.Guild.Id]; if (!server.EnableCities) return; @@ -366,7 +366,7 @@ private async Task Client_GuildMemberUpdated(GuildMemberUpdateEventArgs e) // if (e.Author.Id == e.Client.CurrentUser.Id) // return; - // if (_whConfig.BotChannelIds.Count > 0 && !_whConfig.BotChannelIds.Contains(e.Channel.Id)) + // if (_whConfig.Instance.BotChannelIds.Count > 0 && !_whConfig.Instance.BotChannelIds.Contains(e.Channel.Id)) // return; // await _commands.HandleCommandsAsync(e); @@ -414,8 +414,8 @@ private async Task Commands_CommandErrored(CommandErrorEventArgs e) // The user lacks required permissions, var emoji = DiscordEmoji.FromName(e.Context.Client, ":x:"); - var guildId = e.Context.Guild?.Id ?? e.Context.Client.Guilds.FirstOrDefault(x => _whConfig.Servers.ContainsKey(x.Key)).Key; - var prefix = _whConfig.Servers.ContainsKey(guildId) ? _whConfig.Servers[guildId].CommandPrefix : "!"; + var guildId = e.Context.Guild?.Id ?? e.Context.Client.Guilds.FirstOrDefault(x => _whConfig.Instance.Servers.ContainsKey(x.Key)).Key; + var prefix = _whConfig.Instance.Servers.ContainsKey(guildId) ? _whConfig.Instance.Servers[guildId].CommandPrefix : "!"; var example = $"Command Example: ```{prefix}{e.Command.Name} {string.Join(" ", e.Command.Arguments.Select(x => x.IsOptional ? $"[{x.Name}]" : x.Name))}```\r\n*Parameters in brackets are optional.*"; // let's wrap the response into an embed @@ -513,14 +513,14 @@ private void OnPokemonAlarmTriggered(object sender, AlarmEventTriggeredEventArgs if (!_servers.ContainsKey(e.GuildId)) return; - if (!_whConfig.Servers.ContainsKey(e.GuildId)) + if (!_whConfig.Instance.Servers.ContainsKey(e.GuildId)) return; try { - var server = _whConfig.Servers[e.GuildId]; + var server = _whConfig.Instance.Servers[e.GuildId]; var client = _servers[e.GuildId]; - var eb = pokemon.GeneratePokemonMessage(e.GuildId, client, _whConfig, e.Alarm, loc.Name); + var eb = pokemon.GeneratePokemonMessage(e.GuildId, client, _whConfig.Instance, e.Alarm, loc.Name); var jsonEmbed = new DiscordWebhookMessage { Username = eb.Username, @@ -560,14 +560,14 @@ private void OnRaidAlarmTriggered(object sender, AlarmEventTriggeredEventArgs { try { - _whConfig = WhConfig.Load(e.FullPath); + _whConfig.Instance = WhConfig.Load(e.FullPath); } catch (Exception ex) { @@ -1100,7 +1100,7 @@ private async void UnhandledExceptionHandler(object sender, UnhandledExceptionEv if (e.IsTerminating) { - foreach (var (guildId, serverConfig) in _whConfig.Servers) + foreach (var (guildId, serverConfig) in _whConfig.Instance.Servers) { if (!_servers.ContainsKey(guildId)) { diff --git a/src/Commands/Dependencies.cs b/src/Commands/Dependencies.cs index a0ed1964..16538e3a 100644 --- a/src/Commands/Dependencies.cs +++ b/src/Commands/Dependencies.cs @@ -9,24 +9,26 @@ public class Dependencies { + private readonly WhConfigHolder _configHolder; + public InteractivityModule Interactivity; public WebhookController Whm { get; } public SubscriptionProcessor SubscriptionProcessor { get; } - public WhConfig WhConfig { get; } + public WhConfig WhConfig => _configHolder.Instance; public StripeService Stripe { get; } public OsmManager OsmManager { get; } - public Dependencies(InteractivityModule interactivity, WebhookController whm, SubscriptionProcessor subProcessor, WhConfig whConfig, StripeService stripe) + public Dependencies(InteractivityModule interactivity, WebhookController whm, SubscriptionProcessor subProcessor, WhConfigHolder whConfig, StripeService stripe) { Interactivity = interactivity; Whm = whm; SubscriptionProcessor = subProcessor; - WhConfig = whConfig; + _configHolder = whConfig; Stripe = stripe; OsmManager = new OsmManager(); } diff --git a/src/Commands/Input/SubscriptionInput.cs b/src/Commands/Input/SubscriptionInput.cs index 217a56d4..4a1800e8 100644 --- a/src/Commands/Input/SubscriptionInput.cs +++ b/src/Commands/Input/SubscriptionInput.cs @@ -1,4 +1,6 @@ -namespace WhMgr.Commands.Input +using WhMgr.Configuration; + +namespace WhMgr.Commands.Input { using System; using System.Collections.Generic; @@ -35,13 +37,16 @@ public async Task GetPokemonResult() return validation; } - public async Task> GetAreasResult(List validAreas) + public async Task> GetAreasResult(ulong guildId) { + var deps = _context.Dependencies.GetDependency(); + var server = deps.WhConfig.Servers[guildId]; + var validAreas = server.EnableCities ? server.CityRoles : server.Geofences.Select(g => g.Name).ToList(); var message = (await _context.RespondEmbed($"Enter the areas to get notifications from separated by a comma (i.e. `city1,city2`):\n**Available Areas:**\n{string.Join("\n- ", validAreas)}\n- All", DiscordColor.Blurple)).FirstOrDefault(); var cities = await _context.WaitForUserChoice(true); // Check if gender is a valid gender provided - var areas = SubscriptionAreas.GetAreas(cities, validAreas); + var areas = SubscriptionAreas.GetAreas(server, cities); if (areas.Count == 0) { // No valid areas provided diff --git a/src/Commands/Nests.cs b/src/Commands/Nests.cs index 0b8ec7ba..100ee5a5 100644 --- a/src/Commands/Nests.cs +++ b/src/Commands/Nests.cs @@ -132,7 +132,7 @@ public async Task PostNestsAsync(CommandContext ctx, try { var eb = GenerateNestMessage(guildId, ctx.Client, nest); - var geofence = _dep.Whm.GetGeofence(nest.Latitude, nest.Longitude); + var geofence = _dep.Whm.GetGeofence(guildId, nest.Latitude, nest.Longitude); if (geofence == null) { //_logger.Warn($"Failed to find geofence for nest {nest.Key}."); @@ -195,7 +195,7 @@ public IReadOnlyDictionary GetProperties(DiscordGuild guild, Nes var wazeMapsLink = string.Format(Strings.WazeMaps, nest.Latitude, nest.Longitude); var scannerMapsLink = string.Format(_dep.WhConfig.Urls.ScannerMap, nest.Latitude, nest.Longitude); var staticMapLink = StaticMap.GetUrl(_dep.WhConfig.Urls.StaticMap, _dep.WhConfig.StaticMaps["nests"], nest.Latitude, nest.Longitude, pkmnImage, Net.Models.PokemonTeam.All, _dep.OsmManager.GetNest(nest.Name)?.FirstOrDefault()); - var geofence = _dep.Whm.GetGeofence(nest.Latitude, nest.Longitude); + var geofence = _dep.Whm.GetGeofence(guild.Id, nest.Latitude, nest.Longitude); var city = geofence?.Name ?? "Unknown"; var address = new Location(null, city, nest.Latitude, nest.Longitude).GetAddress(_dep.WhConfig); @@ -248,7 +248,7 @@ private Dictionary> GroupNests(ulong guildId, IEnumerable>(); foreach (var nest in nests) { - var geofence = _dep.Whm.GetGeofence(nest.Latitude, nest.Longitude); + var geofence = _dep.Whm.GetGeofence(guildId, nest.Latitude, nest.Longitude); if (geofence == null) { _logger.Warn($"Failed to find geofence for nest {nest.Name}."); diff --git a/src/Commands/Notifications.cs b/src/Commands/Notifications.cs index 3fe4deea..787f629b 100644 --- a/src/Commands/Notifications.cs +++ b/src/Commands/Notifications.cs @@ -1,4 +1,6 @@ -namespace WhMgr.Commands +using WhMgr.Configuration; + +namespace WhMgr.Commands { using System; using System.Collections.Generic; @@ -378,7 +380,7 @@ public async Task PokeMeAsync(CommandContext ctx, return; } - var areas = SubscriptionAreas.GetAreas(city, server.CityRoles); + var areas = SubscriptionAreas.GetAreas(server, city); // Loop through each valid pokemon entry provided foreach (var (pokemonId, form) in validation.Valid) @@ -536,7 +538,7 @@ public async Task PokeMeNotAsync(CommandContext ctx, return; } - var areas = SubscriptionAreas.GetAreas(city, _dep.WhConfig.Servers[guildId].CityRoles); + var areas = SubscriptionAreas.GetAreas(_dep.WhConfig.Servers[guildId], city); var pokemonNames = validation.Valid.Select(x => MasterFile.Instance.Pokedex[x.Key].Name + (string.IsNullOrEmpty(x.Value) ? string.Empty : "-" + x.Value)); var error = false; foreach (var (pokemonId, form) in validation.Valid) @@ -619,7 +621,7 @@ public async Task RaidMeAsync(CommandContext ctx, return; } - var areas = SubscriptionAreas.GetAreas(city, server.CityRoles); + var areas = SubscriptionAreas.GetAreas(server, city); foreach (var (pokemonId, form) in validation.Valid) { var subRaid = subscription.Raids.FirstOrDefault(x => x.PokemonId == pokemonId && string.Compare(x.Form, form, true) == 0); @@ -709,7 +711,7 @@ await ctx.RespondEmbed(Translator.Instance.Translate("ERROR_NO_RAID_SUBSCRIPTION return; } - var areas = SubscriptionAreas.GetAreas(city, _dep.WhConfig.Servers[guildId].CityRoles); + var areas = SubscriptionAreas.GetAreas(_dep.WhConfig.Servers[guildId], city); foreach (var item in validation.Valid) { var pokemonId = item.Key; @@ -784,7 +786,7 @@ public async Task QuestMeAsync(CommandContext ctx, return; } - var areas = SubscriptionAreas.GetAreas(city, server.CityRoles); + var areas = SubscriptionAreas.GetAreas(server, city); var subQuest = subscription.Quests.FirstOrDefault(x => string.Compare(x.RewardKeyword, rewardKeyword, true) == 0); if (subQuest != null) { @@ -867,7 +869,7 @@ await ctx.RespondEmbed(Translator.Instance.Translate("ERROR_NO_QUEST_SUBSCRIPTIO return; } - var areas = SubscriptionAreas.GetAreas(city, _dep.WhConfig.Servers[guildId].CityRoles); + var areas = SubscriptionAreas.GetAreas(_dep.WhConfig.Servers[guildId], city); var subQuest = subscription.Quests.FirstOrDefault(x => string.Compare(x.RewardKeyword, rewardKeyword, true) == 0); // Check if subscribed if (subQuest == null) @@ -1024,7 +1026,7 @@ public async Task InvMeAsync(CommandContext ctx, return; } - var areas = SubscriptionAreas.GetAreas(city, server.CityRoles); + var areas = SubscriptionAreas.GetAreas(server, city); foreach (var (pokemonId, form) in validation.Valid) { var subInvasion = subscription.Invasions.FirstOrDefault(x => x.RewardPokemonId == pokemonId); @@ -1113,7 +1115,7 @@ await ctx.RespondEmbed(Translator.Instance.Translate("ERROR_NO_INVASION_SUBSCRIP return; } - var areas = SubscriptionAreas.GetAreas(city, _dep.WhConfig.Servers[guildId].CityRoles); + var areas = SubscriptionAreas.GetAreas(_dep.WhConfig.Servers[guildId], city); foreach (var item in validation.Valid) { var pokemonId = item.Key; @@ -1275,7 +1277,7 @@ public async Task PvpMeAsync(CommandContext ctx, return; } - var areas = SubscriptionAreas.GetAreas(city, server.CityRoles); + var areas = SubscriptionAreas.GetAreas(server, city); foreach (var (pokemonId, form) in validation.Valid) { if (!MasterFile.Instance.Pokedex.ContainsKey(pokemonId)) @@ -1421,7 +1423,7 @@ public async Task PvpMeNotAsync(CommandContext ctx, return; } - var areas = SubscriptionAreas.GetAreas(city, _dep.WhConfig.Servers[guildId].CityRoles); + var areas = SubscriptionAreas.GetAreas(_dep.WhConfig.Servers[guildId], city); await RemovePvPSubscription(ctx, subscription, validation, pvpLeague, areas); _dep.SubscriptionProcessor.Manager.ReloadSubscriptions(); } @@ -1466,7 +1468,7 @@ public async Task AddAsync(CommandContext ctx) var ivResult = await pkmnInput.GetIVResult(); var lvlResult = await pkmnInput.GetLevelResult(); var genderResult = await pkmnInput.GetGenderResult(); - var areasResult = await pkmnInput.GetAreasResult(_dep.WhConfig.Servers[guildId].CityRoles); + var areasResult = await pkmnInput.GetAreasResult(guildId); var validPokemonNames = string.Join(", ", pkmnResult.Valid.Keys); var result = await AddPokemonSubscription(ctx, subscription, pkmnResult, ivResult, lvlResult.MinimumLevel, lvlResult.MaximumLevel, genderResult, areasResult); @@ -1525,7 +1527,7 @@ await ctx.RespondEmbed var pvpLeague = await pvpInput.GetLeagueResult(); var pvpRank = await pvpInput.GetRankResult(); var pvpPercent = await pvpInput.GetPercentResult(); - var pvpAreas = await pvpInput.GetAreasResult(server.CityRoles); + var pvpAreas = await pvpInput.GetAreasResult(guildId); var validPokemonNames = string.Join(", ", pvpPokemon.Valid.Keys); var pvpResult = await AddPvPSubscription(ctx, subscription, pvpPokemon, pvpLeague, pvpRank, pvpPercent, pvpAreas); @@ -1573,7 +1575,7 @@ await ctx.RespondEmbed var raidInput = new RaidSubscriptionInput(ctx); var raidPokemon = await raidInput.GetPokemonResult(); - var raidAreas = await raidInput.GetAreasResult(server.CityRoles); + var raidAreas = await raidInput.GetAreasResult(guildId); var validPokemonNames = string.Join(", ", raidPokemon.Valid.Select(x => MasterFile.Instance.Pokedex[x.Key].Name + (string.IsNullOrEmpty(x.Value) ? string.Empty : "-" + x.Value))); var raidResult = AddRaidSubscription(ctx, subscription, raidPokemon, raidAreas); @@ -1620,7 +1622,7 @@ await ctx.RespondEmbed(Translator.Instance.Translate("SUCCESS_RAID_SUBSCRIPTIONS var questInput = new QuestSubscriptionInput(ctx); var rewardKeyword = await questInput.GetRewardInput(); - var areas = await questInput.GetAreasResult(server.CityRoles); + var areas = await questInput.GetAreasResult(guildId); var subQuest = subscription.Quests.FirstOrDefault(x => string.Compare(x.RewardKeyword, rewardKeyword, true) == 0); if (subQuest != null) @@ -1673,7 +1675,7 @@ await ctx.RespondEmbed(Translator.Instance.Translate("SUCCESS_QUEST_SUBSCRIPTION var invasionInput = new InvasionSubscriptionInput(ctx); var invasionPokemon = await invasionInput.GetPokemonResult(); - var invasionAreas = await invasionInput.GetAreasResult(server.CityRoles); + var invasionAreas = await invasionInput.GetAreasResult(guildId); var validPokemonNames = string.Join(", ", invasionPokemon.Valid.Select(x => MasterFile.Instance.Pokedex[x.Key].Name)); foreach (var (pokemonId, form) in invasionPokemon.Valid) @@ -1998,7 +2000,7 @@ public async Task RemoveAsync(CommandContext ctx) var pkmnInput = new PokemonSubscriptionInput(ctx); var pkmnResult = await pkmnInput.GetPokemonResult(); - var areasResult = await pkmnInput.GetAreasResult(_dep.WhConfig.Servers[guildId].CityRoles); + var areasResult = await pkmnInput.GetAreasResult(guildId); await RemovePokemonSubscription(ctx, subscription, pkmnResult, areasResult); break; @@ -2016,7 +2018,7 @@ public async Task RemoveAsync(CommandContext ctx) var pvpInput = new PvPSubscriptionInput(ctx); var pvpPokemonResult = await pvpInput.GetPokemonResult(); var pvpLeagueResult = await pvpInput.GetLeagueResult(); - var pvpAreasResult = await pvpInput.GetAreasResult(_dep.WhConfig.Servers[guildId].CityRoles); + var pvpAreasResult = await pvpInput.GetAreasResult(guildId); await RemovePvPSubscription(ctx, subscription, pvpPokemonResult, pvpLeagueResult, pvpAreasResult); } @@ -2033,7 +2035,7 @@ public async Task RemoveAsync(CommandContext ctx) var raidInput = new RaidSubscriptionInput(ctx); var raidPokemonResult = await raidInput.GetPokemonResult(); - var raidAreasResult = await raidInput.GetAreasResult(server.CityRoles); + var raidAreasResult = await raidInput.GetAreasResult(guildId); await RemoveRaidSubscription(ctx, subscription, null, raidAreasResult); } @@ -2050,7 +2052,7 @@ public async Task RemoveAsync(CommandContext ctx) var questInput = new QuestSubscriptionInput(ctx); var rewardResult = await questInput.GetRewardInput(); - var areasResult = await questInput.GetAreasResult(server.CityRoles); + var areasResult = await questInput.GetAreasResult(guildId); var notSubscribed = new List(); var unsubscribed = new List(); @@ -2120,7 +2122,7 @@ await ctx.RespondEmbed(Translator.Instance.Translate("SUCCESS_QUEST_SUBSCRIPTION var invasionInput = new InvasionSubscriptionInput(ctx); var invasionPokemonResult = await invasionInput.GetPokemonResult(); - var invasionAreasResult = await invasionInput.GetAreasResult(server.CityRoles); + var invasionAreasResult = await invasionInput.GetAreasResult(guildId); foreach (var item in invasionPokemonResult.Valid) { @@ -2976,9 +2978,10 @@ private async Task CanExecute(CommandContext ctx) internal class SubscriptionAreas { - public static List GetAreas(string city, List validCities) + public static List GetAreas(DiscordServerConfig server, string city) { // Parse user defined cities + var validCities = server.EnableCities ? server.CityRoles : server.Geofences.Select(g => g.Name).ToList(); var cities = string.IsNullOrEmpty(city) || string.Compare(city, Strings.All, true) == 0 ? validCities : city.Replace(" ,", ",").Replace(", ", ",").Split(',').ToList(); diff --git a/src/Configuration/DiscordServerConfig.cs b/src/Configuration/DiscordServerConfig.cs index 1563bcbe..f0832f15 100644 --- a/src/Configuration/DiscordServerConfig.cs +++ b/src/Configuration/DiscordServerConfig.cs @@ -1,4 +1,6 @@ -namespace WhMgr.Configuration +using WhMgr.Geofence; + +namespace WhMgr.Configuration { using System; using System.Collections.Generic; @@ -58,6 +60,15 @@ public class DiscordServerConfig /// [JsonProperty("alarms")] public string AlarmsFile { get; set; } + + /// + /// Gets or sets the list of Geofence files to use for the Discord server (in addition to the common ones) + /// + [JsonProperty("geofences")] + public string[] GeofenceFiles { get; set; } + + [JsonIgnore] + public List Geofences { get; } = new List(); /// /// Gets or sets whether to enable custom direct message subscriptions diff --git a/src/Configuration/WhConfigHolder.cs b/src/Configuration/WhConfigHolder.cs new file mode 100644 index 00000000..482233df --- /dev/null +++ b/src/Configuration/WhConfigHolder.cs @@ -0,0 +1,48 @@ +using System; + +namespace WhMgr.Configuration +{ + /// + /// This class holds a singleton instance of WhConfig which can be swapped out (e.g. after a config reload) without everybody + /// needing to update their references to the config itself. + /// + public class WhConfigHolder + { + private readonly object _instanceMutex = new object(); + + private WhConfig _instance; + + public WhConfigHolder(WhConfig instance) + { + _instance = instance; + } + + /// + /// Fired after the config instance was swapped for a new one + /// + public event Action Reloaded; + + /// + /// Provides thread-safe access to the internal WhConfig instance + /// + public WhConfig Instance + { + get + { + WhConfig value; + + lock (_instanceMutex) + value = _instance; + + return value; + } + set + { + lock (_instanceMutex) + _instance = value; + + Reloaded?.Invoke(); + } + } + } +} diff --git a/src/Data/Subscriptions/SubscriptionManager.cs b/src/Data/Subscriptions/SubscriptionManager.cs index 36b4dc93..0565d51c 100644 --- a/src/Data/Subscriptions/SubscriptionManager.cs +++ b/src/Data/Subscriptions/SubscriptionManager.cs @@ -20,7 +20,7 @@ public class SubscriptionManager private static readonly IEventLogger _logger = EventLogger.GetLogger("MANAGER", Program.LogLevel); - private readonly WhConfig _whConfig; + private readonly WhConfigHolder _whConfig; private List _subscriptions; private readonly OrmLiteConnectionFactory _connFactory; @@ -41,35 +41,37 @@ public class SubscriptionManager #region Constructor - public SubscriptionManager(WhConfig whConfig) + public SubscriptionManager(WhConfigHolder whConfig) { _logger.Trace($"SubscriptionManager::SubscriptionManager"); _whConfig = whConfig; - if (_whConfig?.Database?.Main == null) + if (_whConfig.Instance?.Database?.Main == null) { var err = "Main database is not configured in config.json file."; _logger.Error(err); throw new NullReferenceException(err); } - if (_whConfig?.Database?.Scanner == null) + if (_whConfig.Instance?.Database?.Scanner == null) { var err = "Scanner database is not configured in config.json file."; _logger.Error(err); throw new NullReferenceException(err); } - if (_whConfig?.Database?.Nests == null) + _connFactory = new OrmLiteConnectionFactory(_whConfig.Instance.Database.Main.ToString(), MySqlDialect.Provider); + + if (_whConfig.Instance.Database?.Nests == null) { _logger.Warn("Nest database is not configured in config.json file, nest alarms and commands will not work."); } - _connFactory = new OrmLiteConnectionFactory(_whConfig.Database.Main.ToString(), MySqlDialect.Provider); + _connFactory = new OrmLiteConnectionFactory(_whConfig.Instance.Database.Main.ToString(), MySqlDialect.Provider); // Reload subscriptions every 60 seconds to account for UI changes - _reloadTimer = new Timer(_whConfig.ReloadSubscriptionChangesMinutes * 60 * 1000); + _reloadTimer = new Timer(_whConfig.Instance.ReloadSubscriptionChangesMinutes * 60 * 1000); _reloadTimer.Elapsed += (sender, e) => ReloadSubscriptions(); _reloadTimer.Start(); diff --git a/src/Data/Subscriptions/SubscriptionProcessor.cs b/src/Data/Subscriptions/SubscriptionProcessor.cs index f28e608c..586e8f21 100644 --- a/src/Data/Subscriptions/SubscriptionProcessor.cs +++ b/src/Data/Subscriptions/SubscriptionProcessor.cs @@ -1,4 +1,6 @@ -namespace WhMgr.Data.Subscriptions +using WhMgr.Geofence; + +namespace WhMgr.Data.Subscriptions { using System; using System.Collections.Generic; @@ -30,7 +32,7 @@ public class SubscriptionProcessor private static readonly IEventLogger _logger = EventLogger.GetLogger("SUBSCRIPTION", Program.LogLevel); private readonly Dictionary _servers; - private readonly WhConfig _whConfig; + private readonly WhConfigHolder _whConfig; private readonly WebhookController _whm; private readonly NotificationQueue _queue; @@ -53,7 +55,7 @@ public class SubscriptionProcessor /// Discord servers dictionary /// Configuration file /// Webhook controller class - public SubscriptionProcessor(Dictionary servers, WhConfig config, WebhookController whm) + public SubscriptionProcessor(Dictionary servers, WhConfigHolder config, WebhookController whm) { _logger.Trace($"SubscriptionProcessor::SubscriptionProcessor"); @@ -76,11 +78,18 @@ public async Task ProcessPokemonSubscription(PokemonData pkmn) if (!MasterFile.Instance.Pokedex.ContainsKey(pkmn.Id)) return; - var loc = _whm.GetGeofence(pkmn.Latitude, pkmn.Longitude); - if (loc == null) + // Cache the result per-guild so that geospatial stuff isn't queried for every single subscription below + Dictionary locationCache = new Dictionary(); + + GeofenceItem GetGeofence(ulong guildId) { - //_logger.Warn($"Failed to lookup city from coordinates {pkmn.Latitude},{pkmn.Longitude} {db.Pokemon[pkmn.Id].Name} {pkmn.IV}, skipping..."); - return; + if (!locationCache.TryGetValue(guildId, out var geofence)) + { + geofence = _whm.GetGeofence(guildId, pkmn.Latitude, pkmn.Longitude); + locationCache.Add(guildId, geofence); + } + + return geofence; } var subscriptions = Manager.GetUserSubscriptionsByPokemonId(pkmn.Id); @@ -109,10 +118,10 @@ public async Task ProcessPokemonSubscription(PokemonData pkmn) if (!user.Enabled) continue; - if (!_whConfig.Servers.ContainsKey(user.GuildId)) + if (!_whConfig.Instance.Servers.ContainsKey(user.GuildId)) continue; - if (!_whConfig.Servers[user.GuildId].Subscriptions.Enabled) + if (!_whConfig.Instance.Servers[user.GuildId].Subscriptions.Enabled) continue; if (!_servers.ContainsKey(user.GuildId)) @@ -131,10 +140,10 @@ public async Task ProcessPokemonSubscription(PokemonData pkmn) continue; } - if (member?.Roles == null || loc == null) + if (member?.Roles == null) continue; - if (!member.HasSupporterRole(_whConfig.Servers[user.GuildId].DonorRoleIds)) + if (!member.HasSupporterRole(_whConfig.Instance.Servers[user.GuildId].DonorRoleIds)) { _logger.Debug($"User {member?.Username} ({user.UserId}) is not a supporter, skipping pokemon {pokemon.Name}..."); // Automatically disable users subscriptions if not supporter to prevent issues @@ -167,17 +176,24 @@ public async Task ProcessPokemonSubscription(PokemonData pkmn) )) continue; + var geofence = GetGeofence(user.GuildId); + if (geofence == null) + { + //_logger.Warn($"Failed to lookup city from coordinates {pkmn.Latitude},{pkmn.Longitude} {db.Pokemon[pkmn.Id].Name} {pkmn.IV}, skipping..."); + return; + } + var distanceMatches = user.DistanceM > 0 && user.DistanceM > new Coordinates(user.Latitude, user.Longitude).DistanceTo(new Coordinates(pkmn.Latitude, pkmn.Longitude)); - var geofenceMatches = subscribedPokemon.Areas.Select(x => x.ToLower()).Contains(loc.Name.ToLower()); + var geofenceMatches = subscribedPokemon.Areas.Select(x => x.ToLower()).Contains(geofence.Name.ToLower()); // If set distance does not match and no geofences match, then skip Pokemon... if (!distanceMatches && !geofenceMatches) continue; - var embed = pkmn.GeneratePokemonMessage(user.GuildId, client, _whConfig, null, loc.Name); + var embed = pkmn.GeneratePokemonMessage(user.GuildId, client, _whConfig.Instance, null, geofence.Name); foreach (var emb in embed.Embeds) { - _queue.Enqueue(new NotificationItem(user, member, emb, pokemon.Name, loc.Name, pkmn)); + _queue.Enqueue(new NotificationItem(user, member, emb, pokemon.Name, geofence.Name, pkmn)); } Statistics.Instance.SubscriptionPokemonSent++; @@ -193,7 +209,6 @@ public async Task ProcessPokemonSubscription(PokemonData pkmn) subscriptions = null; member = null; user = null; - loc = null; pokemon = null; await Task.CompletedTask; @@ -204,11 +219,18 @@ public async Task ProcessPvPSubscription(PokemonData pkmn) if (!MasterFile.Instance.Pokedex.ContainsKey(pkmn.Id)) return; - var loc = _whm.GetGeofence(pkmn.Latitude, pkmn.Longitude); - if (loc == null) + // Cache the result per-guild so that geospatial stuff isn't queried for every single subscription below + Dictionary locationCache = new Dictionary(); + + GeofenceItem GetGeofence(ulong guildId) { - //_logger.Warn($"Failed to lookup city from coordinates {pkmn.Latitude},{pkmn.Longitude} {db.Pokemon[pkmn.Id].Name} {pkmn.IV}, skipping..."); - return; + if (!locationCache.TryGetValue(guildId, out var geofence)) + { + geofence = _whm.GetGeofence(guildId, pkmn.Latitude, pkmn.Longitude); + locationCache.Add(guildId, geofence); + } + + return geofence; } var subscriptions = Manager.GetUserSubscriptionsByPvPPokemonId(pkmn.Id); @@ -235,10 +257,10 @@ public async Task ProcessPvPSubscription(PokemonData pkmn) if (!user.Enabled) continue; - if (!_whConfig.Servers.ContainsKey(user.GuildId)) + if (!_whConfig.Instance.Servers.ContainsKey(user.GuildId)) continue; - if (!_whConfig.Servers[user.GuildId].Subscriptions.Enabled) + if (!_whConfig.Instance.Servers[user.GuildId].Subscriptions.Enabled) continue; if (!_servers.ContainsKey(user.GuildId)) @@ -257,10 +279,10 @@ public async Task ProcessPvPSubscription(PokemonData pkmn) continue; } - if (member?.Roles == null || loc == null) + if (member?.Roles == null) continue; - if (!member.HasSupporterRole(_whConfig.Servers[user.GuildId].DonorRoleIds)) + if (!member.HasSupporterRole(_whConfig.Instance.Servers[user.GuildId].DonorRoleIds)) { _logger.Debug($"User {member?.Username} ({user.UserId}) is not a supporter, skipping pvp pokemon {pokemon.Name}..."); // Automatically disable users subscriptions if not supporter to prevent issues @@ -294,17 +316,24 @@ public async Task ProcessPvPSubscription(PokemonData pkmn) if (!matchesGreat && !matchesUltra) continue; + var geofence = GetGeofence(user.GuildId); + if (geofence == null) + { + //_logger.Warn($"Failed to lookup city from coordinates {pkmn.Latitude},{pkmn.Longitude} {db.Pokemon[pkmn.Id].Name} {pkmn.IV}, skipping..."); + return; + } + var distanceMatches = user.DistanceM > 0 && user.DistanceM > new Coordinates(user.Latitude, user.Longitude).DistanceTo(new Coordinates(pkmn.Latitude, pkmn.Longitude)); - var geofenceMatches = subscribedPokemon.Areas.Select(x => x.ToLower()).Contains(loc.Name.ToLower()); + var geofenceMatches = subscribedPokemon.Areas.Select(x => x.ToLower()).Contains(geofence.Name.ToLower()); // If set distance does not match and no geofences match, then skip Pokemon... if (!distanceMatches && !geofenceMatches) continue; - var embed = pkmn.GeneratePokemonMessage(user.GuildId, client, _whConfig, null, loc.Name); + var embed = pkmn.GeneratePokemonMessage(user.GuildId, client, _whConfig.Instance, null, geofence.Name); foreach (var emb in embed.Embeds) { - _queue.Enqueue(new NotificationItem(user, member, emb, pokemon.Name, loc.Name)); + _queue.Enqueue(new NotificationItem(user, member, emb, pokemon.Name, geofence.Name)); } Statistics.Instance.SubscriptionPokemonSent++; @@ -320,7 +349,6 @@ public async Task ProcessPvPSubscription(PokemonData pkmn) subscriptions = null; member = null; user = null; - loc = null; pokemon = null; await Task.CompletedTask; @@ -331,11 +359,18 @@ public async Task ProcessRaidSubscription(RaidData raid) if (!MasterFile.Instance.Pokedex.ContainsKey(raid.PokemonId)) return; - var loc = _whm.GetGeofence(raid.Latitude, raid.Longitude); - if (loc == null) + // Cache the result per-guild so that geospatial stuff isn't queried for every single subscription below + Dictionary locationCache = new Dictionary(); + + GeofenceItem GetGeofence(ulong guildId) { - //_logger.Warn($"Failed to lookup city for coordinates {raid.Latitude},{raid.Longitude}, skipping..."); - return; + if (!locationCache.TryGetValue(guildId, out var geofence)) + { + geofence = _whm.GetGeofence(guildId, raid.Latitude, raid.Longitude); + locationCache.Add(guildId, geofence); + } + + return geofence; } var subscriptions = Manager.GetUserSubscriptionsByRaidBossId(raid.PokemonId); @@ -358,10 +393,10 @@ public async Task ProcessRaidSubscription(RaidData raid) if (!user.Enabled) continue; - if (!_whConfig.Servers.ContainsKey(user.GuildId)) + if (!_whConfig.Instance.Servers.ContainsKey(user.GuildId)) continue; - if (!_whConfig.Servers[user.GuildId].Subscriptions.Enabled) + if (!_whConfig.Instance.Servers[user.GuildId].Subscriptions.Enabled) continue; if (!_servers.ContainsKey(user.GuildId)) @@ -376,7 +411,7 @@ public async Task ProcessRaidSubscription(RaidData raid) continue; } - if (!member.HasSupporterRole(_whConfig.Servers[user.GuildId].DonorRoleIds)) + if (!member.HasSupporterRole(_whConfig.Instance.Servers[user.GuildId].DonorRoleIds)) { _logger.Info($"User {user.UserId} is not a supporter, skipping raid boss {pokemon.Name}..."); // Automatically disable users subscriptions if not supporter to prevent issues @@ -410,17 +445,24 @@ public async Task ProcessRaidSubscription(RaidData raid) continue; } + var geofence = GetGeofence(user.GuildId); + if (geofence == null) + { + //_logger.Warn($"Failed to lookup city from coordinates {pkmn.Latitude},{pkmn.Longitude} {db.Pokemon[pkmn.Id].Name} {pkmn.IV}, skipping..."); + return; + } + var distanceMatches = user.DistanceM > 0 && user.DistanceM > new Coordinates(user.Latitude, user.Longitude).DistanceTo(new Coordinates(raid.Latitude, raid.Longitude)); - var geofenceMatches = subPkmn.Areas.Select(x => x.ToLower()).Contains(loc.Name.ToLower()); + var geofenceMatches = subPkmn.Areas.Select(x => x.ToLower()).Contains(geofence.Name.ToLower()); // If set distance does not match and no geofences match, then skip Pokemon... if (!distanceMatches && !geofenceMatches) continue; - var embed = raid.GenerateRaidMessage(user.GuildId, client, _whConfig, null, loc.Name); + var embed = raid.GenerateRaidMessage(user.GuildId, client, _whConfig.Instance, null, geofence.Name); foreach (var emb in embed.Embeds) { - _queue.Enqueue(new NotificationItem(user, member, emb, pokemon.Name, loc.Name)); + _queue.Enqueue(new NotificationItem(user, member, emb, pokemon.Name, geofence.Name)); } Statistics.Instance.SubscriptionRaidsSent++; @@ -435,7 +477,6 @@ public async Task ProcessRaidSubscription(RaidData raid) subscriptions.Clear(); subscriptions = null; user = null; - loc = null; await Task.CompletedTask; } @@ -446,11 +487,18 @@ public async Task ProcessQuestSubscription(QuestData quest) var rewardKeyword = quest.GetReward(); var questName = quest.GetQuestMessage(); - var loc = _whm.GetGeofence(quest.Latitude, quest.Longitude); - if (loc == null) + // Cache the result per-guild so that geospatial stuff isn't queried for every single subscription below + Dictionary locationCache = new Dictionary(); + + GeofenceItem GetGeofence(ulong guildId) { - //_logger.Warn($"Failed to lookup city for coordinates {quest.Latitude},{quest.Longitude}, skipping..."); - return; + if (!locationCache.TryGetValue(guildId, out var geofence)) + { + geofence = _whm.GetGeofence(guildId, quest.Latitude, quest.Longitude); + locationCache.Add(guildId, geofence); + } + + return geofence; } var subscriptions = Manager.GetUserSubscriptions(); @@ -473,10 +521,10 @@ public async Task ProcessQuestSubscription(QuestData quest) if (!user.Enabled) continue; - if (!_whConfig.Servers.ContainsKey(user.GuildId)) + if (!_whConfig.Instance.Servers.ContainsKey(user.GuildId)) continue; - if (!_whConfig.Servers[user.GuildId].Subscriptions.Enabled) + if (!_whConfig.Instance.Servers[user.GuildId].Subscriptions.Enabled) continue; if (!_servers.ContainsKey(user.GuildId)) @@ -491,7 +539,7 @@ public async Task ProcessQuestSubscription(QuestData quest) continue; } - isSupporter = member.HasSupporterRole(_whConfig.Servers[user.GuildId].DonorRoleIds); + isSupporter = member.HasSupporterRole(_whConfig.Instance.Servers[user.GuildId].DonorRoleIds); if (!isSupporter) { _logger.Info($"User {user.UserId} is not a supporter, skipping quest {questName}..."); @@ -509,17 +557,24 @@ public async Task ProcessQuestSubscription(QuestData quest) continue; } + var geofence = GetGeofence(user.GuildId); + if (geofence == null) + { + //_logger.Warn($"Failed to lookup city from coordinates {pkmn.Latitude},{pkmn.Longitude} {db.Pokemon[pkmn.Id].Name} {pkmn.IV}, skipping..."); + return; + } + var distanceMatches = user.DistanceM > 0 && user.DistanceM > new Coordinates(user.Latitude, user.Longitude).DistanceTo(new Coordinates(quest.Latitude, quest.Longitude)); - var geofenceMatches = subQuest.Areas.Select(x => x.ToLower()).Contains(loc.Name.ToLower()); + var geofenceMatches = subQuest.Areas.Select(x => x.ToLower()).Contains(geofence.Name.ToLower()); // If set distance does not match and no geofences match, then skip Pokemon... if (!distanceMatches && !geofenceMatches) continue; - var embed = quest.GenerateQuestMessage(user.GuildId, client, _whConfig, null, loc.Name); + var embed = quest.GenerateQuestMessage(user.GuildId, client, _whConfig.Instance, null, geofence.Name); foreach (var emb in embed.Embeds) { - _queue.Enqueue(new NotificationItem(user, member, emb, questName, loc.Name)); + _queue.Enqueue(new NotificationItem(user, member, emb, questName, geofence.Name)); } Statistics.Instance.SubscriptionQuestsSent++; @@ -534,18 +589,24 @@ public async Task ProcessQuestSubscription(QuestData quest) subscriptions.Clear(); subscriptions = null; user = null; - loc = null; await Task.CompletedTask; } public async Task ProcessInvasionSubscription(PokestopData pokestop) { - var loc = _whm.GetGeofence(pokestop.Latitude, pokestop.Longitude); - if (loc == null) + // Cache the result per-guild so that geospatial stuff isn't queried for every single subscription below + Dictionary locationCache = new Dictionary(); + + GeofenceItem GetGeofence(ulong guildId) { - //_logger.Warn($"Failed to lookup city for coordinates {pokestop.Latitude},{pokestop.Longitude}, skipping..."); - return; + if (!locationCache.TryGetValue(guildId, out var geofence)) + { + geofence = _whm.GetGeofence(guildId, pokestop.Latitude, pokestop.Longitude); + locationCache.Add(guildId, geofence); + } + + return geofence; } var invasion = MasterFile.Instance.GruntTypes.ContainsKey(pokestop.GruntType) ? MasterFile.Instance.GruntTypes[pokestop.GruntType] : null; @@ -578,10 +639,10 @@ public async Task ProcessInvasionSubscription(PokestopData pokestop) if (!user.Enabled) continue; - if (!_whConfig.Servers.ContainsKey(user.GuildId)) + if (!_whConfig.Instance.Servers.ContainsKey(user.GuildId)) continue; - if (!_whConfig.Servers[user.GuildId].Subscriptions.Enabled) + if (!_whConfig.Instance.Servers[user.GuildId].Subscriptions.Enabled) continue; if (!_servers.ContainsKey(user.GuildId)) @@ -596,7 +657,7 @@ public async Task ProcessInvasionSubscription(PokestopData pokestop) continue; } - if (!member.HasSupporterRole(_whConfig.Servers[user.GuildId].DonorRoleIds)) + if (!member.HasSupporterRole(_whConfig.Instance.Servers[user.GuildId].DonorRoleIds)) { _logger.Info($"User {user.UserId} is not a supporter, skipping Team Rocket invasion {pokestop.Name}..."); // Automatically disable users subscriptions if not supporter to prevent issues @@ -613,17 +674,24 @@ public async Task ProcessInvasionSubscription(PokestopData pokestop) continue; } + var geofence = GetGeofence(user.GuildId); + if (geofence == null) + { + //_logger.Warn($"Failed to lookup city from coordinates {pkmn.Latitude},{pkmn.Longitude} {db.Pokemon[pkmn.Id].Name} {pkmn.IV}, skipping..."); + return; + } + var distanceMatches = user.DistanceM > 0 && user.DistanceM > new Coordinates(user.Latitude, user.Longitude).DistanceTo(new Coordinates(pokestop.Latitude, pokestop.Longitude)); - var geofenceMatches = subInvasion.Areas.Select(x => x.ToLower()).Contains(loc.Name.ToLower()); + var geofenceMatches = subInvasion.Areas.Select(x => x.ToLower()).Contains(geofence.Name.ToLower()); // If set distance does not match and no geofences match, then skip Pokemon... if (!distanceMatches && !geofenceMatches) continue; - var embed = pokestop.GeneratePokestopMessage(user.GuildId, client, _whConfig, null, loc?.Name); + var embed = pokestop.GeneratePokestopMessage(user.GuildId, client, _whConfig.Instance, null, geofence?.Name); foreach (var emb in embed.Embeds) { - _queue.Enqueue(new NotificationItem(user, member, emb, pokestop.Name, loc.Name)); + _queue.Enqueue(new NotificationItem(user, member, emb, pokestop.Name, geofence.Name)); } Statistics.Instance.SubscriptionInvasionsSent++; @@ -638,7 +706,6 @@ public async Task ProcessInvasionSubscription(PokestopData pokestop) subscriptions.Clear(); subscriptions = null; user = null; - loc = null; await Task.CompletedTask; } @@ -671,7 +738,7 @@ private void ProcessQueue() continue; // Check if user is receiving messages too fast. - var maxNotificationsPerMinute = _whConfig.MaxNotificationsPerMinute; + var maxNotificationsPerMinute = _whConfig.Instance.MaxNotificationsPerMinute; if (item.Subscription.Limiter.IsLimited(maxNotificationsPerMinute)) { _logger.Warn($"{item.Member.Username} notifications rate limited, waiting {(60 - item.Subscription.Limiter.TimeLeft.TotalSeconds)} seconds...", item.Subscription.Limiter.TimeLeft.TotalSeconds.ToString("N0")); @@ -723,13 +790,13 @@ private void ProcessQueue() if (!string.IsNullOrEmpty(item.Subscription.PhoneNumber)) { // Check if user is in the allowed text message list or server owner - if (_whConfig.Twilio.UserIds.Contains(item.Member.Id) || - _whConfig.Servers[item.Subscription.GuildId].OwnerId == item.Member.Id) + if (_whConfig.Instance.Twilio.UserIds.Contains(item.Member.Id) || + _whConfig.Instance.Servers[item.Subscription.GuildId].OwnerId == item.Member.Id) { // Send text message (max 160 characters) - if (item.Pokemon != null && IsUltraRare(_whConfig.Twilio, item.Pokemon)) + if (item.Pokemon != null && IsUltraRare(_whConfig.Instance.Twilio, item.Pokemon)) { - var result = Utils.SendSmsMessage(StripEmbed(item), _whConfig.Twilio, item.Subscription.PhoneNumber); + var result = Utils.SendSmsMessage(StripEmbed(item), _whConfig.Instance.Twilio, item.Subscription.PhoneNumber); if (!result) { _logger.Error($"Failed to send text message to phone number '{item.Subscription.PhoneNumber}' for user {item.Subscription.UserId}"); diff --git a/src/Geofence/GeofenceService.cs b/src/Geofence/GeofenceService.cs index 643914bb..90e5aa46 100644 --- a/src/Geofence/GeofenceService.cs +++ b/src/Geofence/GeofenceService.cs @@ -1,38 +1,10 @@ -using System; -using System.IO; -using WhMgr.Diagnostics; - -namespace WhMgr.Geofence +namespace WhMgr.Geofence { using System.Collections.Generic; using System.Linq; public static class GeofenceService { - private static readonly IEventLogger _logger = EventLogger.GetLogger("GEOFENCE", Program.LogLevel); - - public static List LoadGeofences(string geofencesFolder) - { - var geofences = new List(); - - foreach (var file in Directory.EnumerateFiles(geofencesFolder)) - { - try - { - var fileGeofences = GeofenceItem.FromFile(file); - - geofences.AddRange(fileGeofences); - } - catch (Exception ex) - { - _logger.Error($"Could not load Geofence file {file}:"); - _logger.Error(ex); - } - } - - return geofences; - } - public static IEnumerable GetGeofences(IEnumerable geofences, Location point) { // Order descending by priority so that when we iterate forwards using FirstOrDefault, higher-priority diff --git a/src/Net/Webhooks/WebhookController.cs b/src/Net/Webhooks/WebhookController.cs index c300b4e1..5e80148e 100644 --- a/src/Net/Webhooks/WebhookController.cs +++ b/src/Net/Webhooks/WebhookController.cs @@ -32,8 +32,7 @@ public class WebhookController private readonly object _geofencesLock = new object(); private readonly HttpServer _http; private readonly Dictionary _alarms; - private readonly IReadOnlyDictionary _servers; - private readonly WhConfig _config; + private readonly WhConfigHolder _config; private readonly Dictionary _weather; private Dictionary _gyms; @@ -41,11 +40,6 @@ public class WebhookController #region Properties - /// - /// All loaded geofences - /// - private List Geofences { get; set; } - /// /// Gyms cache /// @@ -186,29 +180,21 @@ private void OnInvasionSubscriptionTriggered(PokestopData pokestop) /// Instantiate a new class. /// /// configuration class. - public WebhookController(WhConfig config) + public WebhookController(WhConfigHolder config) { - _logger.Trace($"WebhookManager::WebhookManager [Config={config}, Port={config.WebhookPort}, Servers={config.Servers.Count:N0}]"); + _logger.Trace($"WebhookManager::WebhookManager [Config={config}, Port={config.Instance.WebhookPort}, Servers={config.Instance.Servers.Count:N0}]"); _gyms = new Dictionary(); _weather = new Dictionary(); - _servers = config.Servers; _alarms = new Dictionary(); - lock(_geofencesLock) - Geofences = GeofenceService.LoadGeofences(Strings.GeofenceFolder); - - foreach (var server in _servers) - { - if (_alarms.ContainsKey(server.Key)) - continue; - - var alarms = LoadAlarms(server.Value.AlarmsFile); - _alarms.Add(server.Key, alarms); - } _config = config; + _config.Reloaded += OnConfigReloaded; + + LoadGeofences(); + LoadAlarms(); - _http = new HttpServer(_config.ListeningHost, _config.WebhookPort, _config.DespawnTimeMinimumMinutes); + _http = new HttpServer(_config.Instance.ListeningHost, _config.Instance.WebhookPort, _config.Instance.DespawnTimeMinimumMinutes); _http.PokemonReceived += Http_PokemonReceived; _http.RaidReceived += Http_RaidReceived; _http.QuestReceived += Http_QuestReceived; @@ -216,7 +202,7 @@ public WebhookController(WhConfig config) _http.GymReceived += Http_GymReceived; _http.GymDetailsReceived += Http_GymDetailsReceived; _http.WeatherReceived += Http_WeatherReceived; - _http.IsDebug = _config.Debug; + _http.IsDebug = _config.Instance.Debug; new Thread(() => { LoadAlarmsOnChange(); @@ -244,6 +230,13 @@ public void Stop() _http?.Stop(); } + public List GetServerGeofences(ulong guildId) + { + var server = _config.Instance.Servers[guildId]; + + return server.Geofences; + } + #endregion #region HttpServer Events @@ -258,7 +251,7 @@ private void Http_PokemonReceived(object sender, DataReceivedEventArgs 0) + if (_config.Instance.EventPokemonIds.Contains(pkmn.Id) && _config.Instance.EventPokemonIds.Count > 0) { // Skip Pokemon if no IV stats. if (pkmn.IsMissingStats) @@ -266,7 +259,7 @@ private void Http_PokemonReceived(object sender, DataReceivedEventArgs 0 && iv < _config.EventMinimumIV && !pkmn.MatchesGreatLeague && !pkmn.MatchesUltraLeague) + if (iv > 0 && iv < _config.Instance.EventMinimumIV && !pkmn.MatchesGreatLeague && !pkmn.MatchesUltraLeague) return; } @@ -323,10 +316,68 @@ private void Http_WeatherReceived(object sender, DataReceivedEventArgs(); + + if (geofenceFiles != null && geofenceFiles.Any()) + { + foreach (var file in geofenceFiles) + { + var filePath = Path.Combine(Strings.GeofenceFolder, file); + + try + { + var fileGeofences = GeofenceItem.FromFile(filePath); + + geofences.AddRange(fileGeofences); + + _logger.Info($"Successfully loaded {fileGeofences.Count} geofences from {file}"); + } + catch (Exception ex) + { + _logger.Error($"Could not load Geofence file {file} (for server {serverId}):"); + _logger.Error(ex); + } + } + } + + serverConfig.Geofences.AddRange(geofences); + } + } + + #endregion + #region Alarms Initialization - private AlarmList LoadAlarms(string alarmsFilePath) + private void LoadAlarms() + { + _alarms.Clear(); + + foreach (var (serverId, serverConfig) in _config.Instance.Servers) + { + var alarms = LoadAlarms(serverId, serverConfig.AlarmsFile); + + _alarms.Add(serverId, alarms); + } + } + + private AlarmList LoadAlarms(ulong forGuildId, string alarmsFilePath) { _logger.Trace($"WebhookManager::LoadAlarms [AlarmsFilePath={alarmsFilePath}]"); @@ -352,30 +403,46 @@ private AlarmList LoadAlarms(string alarmsFilePath) _logger.Info($"Alarms file {alarmsFilePath} was loaded successfully."); - alarms.Alarms.ForEach(x => + foreach (var alarm in alarms.Alarms) { - if (x.Geofences != null) + if (alarm.Geofences != null) { - foreach (var geofenceName in x.Geofences) + foreach (var geofenceName in alarm.Geofences) { - var geofences = Geofences.Where(g => g.Name.Equals(geofenceName, StringComparison.OrdinalIgnoreCase) || - g.Filename.Equals(geofenceName, StringComparison.OrdinalIgnoreCase)).ToList(); - - if (geofences.Any()) + lock (_geofencesLock) { - x.GeofenceItems.AddRange(geofences); - } - else - { - _logger.Warn($"No geofences were found matching the name or filename \"{geofenceName}\" (for alarm \"{x.Name}\")"); + // First try and find loaded geofences for this server by name or filename (so we don't have to parse already loaded files again) + var server = _config.Instance.Servers[forGuildId]; + var geofences = server.Geofences.Where(g => g.Name.Equals(geofenceName, StringComparison.OrdinalIgnoreCase) || + g.Filename.Equals(geofenceName, StringComparison.OrdinalIgnoreCase)).ToList(); + + if (geofences.Any()) + { + alarm.GeofenceItems.AddRange(geofences); + } + else + { + // Try and load from a file instead + var filePath = Path.Combine(Strings.GeofenceFolder, geofenceName); + + if (!File.Exists(filePath)) + { + _logger.Warn($"Could not find Geofence file \"{geofenceName}\" for alarm \"{alarm.Name}\""); + continue; + } + + var fileGeofences = GeofenceItem.FromFile(filePath); + + alarm.GeofenceItems.AddRange(fileGeofences); + _logger.Info($"Successfully loaded {fileGeofences.Count} geofences from {geofenceName}"); + } } } } - x.LoadAlerts(); - - x.LoadFilters(); - }); + alarm.LoadAlerts(); + alarm.LoadFilters(); + } return alarms; } @@ -384,9 +451,9 @@ private void LoadAlarmsOnChange() { _logger.Trace($"WebhookManager::LoadAlarmsOnChange"); - foreach (var (guildId, serverConfig) in _servers) + foreach (var (guildId, guildConfig) in _config.Instance.Servers) { - var alarmsFile = serverConfig.AlarmsFile; + var alarmsFile = guildConfig.AlarmsFile; var path = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), alarmsFile)); var fileWatcher = new FileWatcher(path); @@ -394,7 +461,7 @@ private void LoadAlarmsOnChange() try { _logger.Debug("Reloading Alarms"); - _alarms[guildId] = LoadAlarms(path); + _alarms[guildId] = LoadAlarms(guildId, path); } catch (Exception ex) { @@ -418,8 +485,8 @@ private void LoadGeofencesOnChange() { _logger.Debug("Reloading Geofences"); - lock (_geofencesLock) - Geofences = GeofenceService.LoadGeofences(Strings.GeofenceFolder); + LoadGeofences(); + LoadAlarms(); // Reload alarms after geofences too } catch (Exception ex) { @@ -983,13 +1050,15 @@ public void SetWeather(long id, GameplayWeather.Types.WeatherCondition type) /// /// Get the geofence the provided location falls within. /// + /// The guild ID in which to look for Geofences /// Latitude geocoordinate /// Longitude geocoordinate /// Returns a object the provided location falls within. - public GeofenceItem GetGeofence(double latitude, double longitude) + public GeofenceItem GetGeofence(ulong guildId, double latitude, double longitude) { - lock (_geofencesLock) - return GeofenceService.GetGeofence(Geofences, new Location(latitude, longitude)); + var server = _config.Instance.Servers[guildId]; + + return GeofenceService.GetGeofence(server.Geofences, new Location(latitude, longitude)); } #endregion diff --git a/src/Program.cs b/src/Program.cs index 38cbc414..a7ef2ee4 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,4 +1,6 @@ -namespace WhMgr +using WhMgr.Configuration; + +namespace WhMgr { using System; using System.Diagnostics; @@ -70,7 +72,7 @@ static async Task MainAsync(string[] args) LogLevel = whConfig.LogLevel; // Start bot - var bot = new Bot(whConfig); + var bot = new Bot(new WhConfigHolder(whConfig)); await bot.Start(); // Keep the process alive diff --git a/test/GeofenceTest.cs b/test/GeofenceTest.cs index 4fc92ab5..424ca6ba 100644 --- a/test/GeofenceTest.cs +++ b/test/GeofenceTest.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using NUnit.Framework; @@ -11,11 +13,33 @@ public class GeofenceTests private const string JsonGeofencesFolder = "JsonGeofences"; private const string IniGeofencesFolder = "IniGeofences"; + private static IEnumerable LoadGeofences(string geofencesFolder) + { + var geofences = new List(); + + foreach (var file in Directory.EnumerateFiles(geofencesFolder)) + { + try + { + var fileGeofences = GeofenceItem.FromFile(file); + + geofences.AddRange(fileGeofences); + } + catch (Exception ex) + { + TestContext.Error.WriteLine($"Could not load Geofence file {file}:"); + TestContext.Error.WriteLine(ex); + } + } + + return geofences; + } + [Test] public void TestLoadingJson() { var effectiveFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, JsonGeofencesFolder); - var geofences = GeofenceService.LoadGeofences(effectiveFolder); + var geofences = LoadGeofences(effectiveFolder); Assert.IsNotEmpty(geofences); } @@ -24,7 +48,7 @@ public void TestLoadingJson() public void TestLoadingIni() { var effectiveFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, IniGeofencesFolder); - var geofences = GeofenceService.LoadGeofences(effectiveFolder); + var geofences = LoadGeofences(effectiveFolder); Assert.IsNotEmpty(geofences); } @@ -37,7 +61,7 @@ public void TestLoadingIni() public void TestInsideJson(double latitude, double longitude, string expectedGeofence) { var effectiveFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, JsonGeofencesFolder); - var geofences = GeofenceService.LoadGeofences(effectiveFolder); + var geofences = LoadGeofences(effectiveFolder); var insideOf = GeofenceService.GetGeofences(geofences, new Location(latitude, longitude)).ToList(); if (!string.IsNullOrEmpty(expectedGeofence)) @@ -60,7 +84,7 @@ public void TestInsideJson(double latitude, double longitude, string expectedGeo public void TestInsideIni(double latitude, double longitude, string expectedGeofence) { var effectiveFolder = Path.Combine(TestContext.CurrentContext.TestDirectory, IniGeofencesFolder); - var geofences = GeofenceService.LoadGeofences(effectiveFolder); + var geofences = LoadGeofences(effectiveFolder); var insideOf = GeofenceService.GetGeofences(geofences, new Location(latitude, longitude)).ToList(); if (!string.IsNullOrEmpty(expectedGeofence))