Reenabled converter commands. Improved rewards reload on bots with multiple shards.
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,6 @@ | ||||
| #Manually added files | ||||
|  | ||||
| patreon_rewards.json | ||||
| command_errors*.txt | ||||
|  | ||||
| src/NadekoBot/Command Errors*.txt | ||||
|   | ||||
| @@ -1,94 +1,93 @@ | ||||
| //using Discord; | ||||
| //using Discord.Commands; | ||||
| //using NadekoBot.Attributes; | ||||
| //using NadekoBot.Extensions; | ||||
| //using NadekoBot.Services.Utility; | ||||
| //using System; | ||||
| //using System.Linq; | ||||
| //using System.Threading.Tasks; | ||||
| ////todo Rewrite | ||||
| //namespace NadekoBot.Modules.Utility | ||||
| //{ | ||||
| //    public partial class Utility | ||||
| //    { | ||||
| //        [Group] | ||||
| //        public class UnitConverterCommands : NadekoSubmodule | ||||
| //        { | ||||
| //            private readonly ConverterService _service; | ||||
| using Discord; | ||||
| using Discord.Commands; | ||||
| using NadekoBot.Attributes; | ||||
| using NadekoBot.Extensions; | ||||
| using NadekoBot.Services.Utility; | ||||
| using System; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| namespace NadekoBot.Modules.Utility | ||||
| { | ||||
|     public partial class Utility | ||||
|     { | ||||
|         [Group] | ||||
|         public class UnitConverterCommands : NadekoSubmodule | ||||
|         { | ||||
|             private readonly ConverterService _service; | ||||
|  | ||||
| //            public UnitConverterCommands(ConverterService service) | ||||
| //            { | ||||
| //                _service = service; | ||||
| //            } | ||||
|             public UnitConverterCommands(ConverterService service) | ||||
|             { | ||||
|                 _service = service; | ||||
|             } | ||||
|  | ||||
| //            [NadekoCommand, Usage, Description, Aliases] | ||||
| //            public async Task ConvertList() | ||||
| //            { | ||||
| //                var res = _service.Units.GroupBy(x => x.UnitType) | ||||
| //                               .Aggregate(new EmbedBuilder().WithTitle(GetText("convertlist")) | ||||
| //                                                            .WithColor(NadekoBot.OkColor), | ||||
| //                                          (embed, g) => embed.AddField(efb => | ||||
| //                                                                         efb.WithName(g.Key.ToTitleCase()) | ||||
| //                                                                         .WithValue(String.Join(", ", g.Select(x => x.Triggers.FirstOrDefault()) | ||||
| //                                                                                                       .OrderBy(x => x))))); | ||||
| //                await Context.Channel.EmbedAsync(res); | ||||
| //            } | ||||
| //            [NadekoCommand, Usage, Description, Aliases] | ||||
| //            public async Task Convert(string origin, string target, decimal value) | ||||
| //            { | ||||
| //                var originUnit = _service.Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(origin.ToLowerInvariant())); | ||||
| //                var targetUnit = _service.Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(target.ToLowerInvariant())); | ||||
| //                if (originUnit == null || targetUnit == null) | ||||
| //                { | ||||
| //                    await ReplyErrorLocalized("convert_not_found", Format.Bold(origin), Format.Bold(target)).ConfigureAwait(false); | ||||
| //                    return; | ||||
| //                } | ||||
| //                if (originUnit.UnitType != targetUnit.UnitType) | ||||
| //                { | ||||
| //                    await ReplyErrorLocalized("convert_type_error", Format.Bold(originUnit.Triggers.First()), Format.Bold(targetUnit.Triggers.First())).ConfigureAwait(false); | ||||
| //                    return; | ||||
| //                } | ||||
| //                decimal res; | ||||
| //                if (originUnit.Triggers == targetUnit.Triggers) res = value; | ||||
| //                else if (originUnit.UnitType == "temperature") | ||||
| //                { | ||||
| //                    //don't really care too much about efficiency, so just convert to Kelvin, then to target | ||||
| //                    switch (originUnit.Triggers.First().ToUpperInvariant()) | ||||
| //                    { | ||||
| //                        case "C": | ||||
| //                            res = value + 273.15m; //celcius! | ||||
| //                            break; | ||||
| //                        case "F": | ||||
| //                            res = (value + 459.67m) * (5m / 9m); | ||||
| //                            break; | ||||
| //                        default: | ||||
| //                            res = value; | ||||
| //                            break; | ||||
| //                    } | ||||
| //                    //from Kelvin to target | ||||
| //                    switch (targetUnit.Triggers.First().ToUpperInvariant()) | ||||
| //                    { | ||||
| //                        case "C": | ||||
| //                            res = res - 273.15m; //celcius! | ||||
| //                            break; | ||||
| //                        case "F": | ||||
| //                            res = res * (9m / 5m) - 459.67m; | ||||
| //                            break; | ||||
| //                    } | ||||
| //                } | ||||
| //                else | ||||
| //                { | ||||
| //                    if (originUnit.UnitType == "currency") | ||||
| //                    { | ||||
| //                        res = (value * targetUnit.Modifier) / originUnit.Modifier; | ||||
| //                    } | ||||
| //                    else | ||||
| //                        res = (value * originUnit.Modifier) / targetUnit.Modifier; | ||||
| //                } | ||||
| //                res = Math.Round(res, 4); | ||||
|             [NadekoCommand, Usage, Description, Aliases] | ||||
|             public async Task ConvertList() | ||||
|             { | ||||
|                 var res = _service.Units.GroupBy(x => x.UnitType) | ||||
|                                .Aggregate(new EmbedBuilder().WithTitle(GetText("convertlist")) | ||||
|                                                             .WithColor(NadekoBot.OkColor), | ||||
|                                           (embed, g) => embed.AddField(efb => | ||||
|                                                                          efb.WithName(g.Key.ToTitleCase()) | ||||
|                                                                          .WithValue(String.Join(", ", g.Select(x => x.Triggers.FirstOrDefault()) | ||||
|                                                                                                        .OrderBy(x => x))))); | ||||
|                 await Context.Channel.EmbedAsync(res); | ||||
|             } | ||||
|             [NadekoCommand, Usage, Description, Aliases] | ||||
|             public async Task Convert(string origin, string target, decimal value) | ||||
|             { | ||||
|                 var originUnit = _service.Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(origin.ToLowerInvariant())); | ||||
|                 var targetUnit = _service.Units.Find(x => x.Triggers.Select(y => y.ToLowerInvariant()).Contains(target.ToLowerInvariant())); | ||||
|                 if (originUnit == null || targetUnit == null) | ||||
|                 { | ||||
|                     await ReplyErrorLocalized("convert_not_found", Format.Bold(origin), Format.Bold(target)).ConfigureAwait(false); | ||||
|                     return; | ||||
|                 } | ||||
|                 if (originUnit.UnitType != targetUnit.UnitType) | ||||
|                 { | ||||
|                     await ReplyErrorLocalized("convert_type_error", Format.Bold(originUnit.Triggers.First()), Format.Bold(targetUnit.Triggers.First())).ConfigureAwait(false); | ||||
|                     return; | ||||
|                 } | ||||
|                 decimal res; | ||||
|                 if (originUnit.Triggers == targetUnit.Triggers) res = value; | ||||
|                 else if (originUnit.UnitType == "temperature") | ||||
|                 { | ||||
|                     //don't really care too much about efficiency, so just convert to Kelvin, then to target | ||||
|                     switch (originUnit.Triggers.First().ToUpperInvariant()) | ||||
|                     { | ||||
|                         case "C": | ||||
|                             res = value + 273.15m; //celcius! | ||||
|                             break; | ||||
|                         case "F": | ||||
|                             res = (value + 459.67m) * (5m / 9m); | ||||
|                             break; | ||||
|                         default: | ||||
|                             res = value; | ||||
|                             break; | ||||
|                     } | ||||
|                     //from Kelvin to target | ||||
|                     switch (targetUnit.Triggers.First().ToUpperInvariant()) | ||||
|                     { | ||||
|                         case "C": | ||||
|                             res = res - 273.15m; //celcius! | ||||
|                             break; | ||||
|                         case "F": | ||||
|                             res = res * (9m / 5m) - 459.67m; | ||||
|                             break; | ||||
|                     } | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     if (originUnit.UnitType == "currency") | ||||
|                     { | ||||
|                         res = (value * targetUnit.Modifier) / originUnit.Modifier; | ||||
|                     } | ||||
|                     else | ||||
|                         res = (value * originUnit.Modifier) / targetUnit.Modifier; | ||||
|                 } | ||||
|                 res = Math.Round(res, 4); | ||||
|  | ||||
| //                await Context.Channel.SendConfirmAsync(GetText("convert", value, (originUnit.Triggers.First()).SnPl(value.IsInteger() ? (int)value : 2), res, (targetUnit.Triggers.First() + "s").SnPl(res.IsInteger() ? (int)res : 2))); | ||||
| //            } | ||||
| //        } | ||||
| //    } | ||||
| //} | ||||
|                 await Context.Channel.SendConfirmAsync(GetText("convert", value, (originUnit.Triggers.First()).SnPl(value.IsInteger() ? (int)value : 2), res, (targetUnit.Triggers.First() + "s").SnPl(res.IsInteger() ? (int)res : 2))); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -290,7 +290,10 @@ namespace NadekoBot | ||||
|  | ||||
|             Task SetClientReady() | ||||
|             { | ||||
|                 clientReady.TrySetResult(true); | ||||
|                 var _ = Task.Run(() => | ||||
|                 { | ||||
|                     clientReady.TrySetResult(true); | ||||
|                 }); | ||||
|                 return Task.CompletedTask; | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -90,5 +90,6 @@ | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <Folder Include="Modules\Music\Classes\" /> | ||||
|     <Folder Include="Utility\Services\" /> | ||||
|   </ItemGroup> | ||||
| </Project> | ||||
|   | ||||
| @@ -126,7 +126,6 @@ namespace NadekoBot.Services.Impl | ||||
|                 return Task.CompletedTask; | ||||
|             }; | ||||
|  | ||||
|             //todo carbonitex update | ||||
|             if (sc != null) | ||||
|             { | ||||
|                 _carbonitexTimer = new Timer(async (state) => | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| using NadekoBot.Services.Database.Models; | ||||
| using Discord.WebSocket; | ||||
| using NadekoBot.Services; | ||||
| using NadekoBot.Services.Database.Models; | ||||
| using Newtonsoft.Json; | ||||
| using NLog; | ||||
| using System; | ||||
| @@ -11,27 +13,29 @@ using System.Threading.Tasks; | ||||
|  | ||||
| namespace NadekoBot.Services.Utility | ||||
| { | ||||
|     //todo periodically load from the database, update only on shard 0 | ||||
|     public class ConverterService | ||||
|     { | ||||
|         public List<ConvertUnit> Units { get; set; } = new List<ConvertUnit>(); | ||||
|         public List<ConvertUnit> Units { get; } = new List<ConvertUnit>(); | ||||
|         private readonly Logger _log; | ||||
|         private Timer _timer; | ||||
|         private readonly Timer _currencyUpdater; | ||||
|         private readonly Timer _currencyLoader; | ||||
|         private readonly TimeSpan _updateInterval = new TimeSpan(12, 0, 0); | ||||
|         private readonly DbService _db; | ||||
|  | ||||
|         public ConverterService(DbService db) | ||||
|         public ConverterService(DiscordSocketClient client, DbService db) | ||||
|         { | ||||
|             _log = LogManager.GetCurrentClassLogger(); | ||||
|             _db = db; | ||||
|             try | ||||
|             { | ||||
|                 var data = JsonConvert.DeserializeObject<List<MeasurementUnit>>(File.ReadAllText("data/units.json")).Select(u => new ConvertUnit() | ||||
|                 { | ||||
|                     Modifier = u.Modifier, | ||||
|                     UnitType = u.UnitType, | ||||
|                     InternalTrigger = string.Join("|", u.Triggers) | ||||
|                 }).ToArray(); | ||||
|                 var data = JsonConvert.DeserializeObject<List<MeasurementUnit>>( | ||||
|                     File.ReadAllText("data/units.json")) | ||||
|                         .Select(u => new ConvertUnit() | ||||
|                         { | ||||
|                             Modifier = u.Modifier, | ||||
|                             UnitType = u.UnitType, | ||||
|                             InternalTrigger = string.Join("|", u.Triggers) | ||||
|                         }).ToArray(); | ||||
|  | ||||
|                 using (var uow = _db.UnitOfWork) | ||||
|                 { | ||||
| @@ -48,10 +52,10 @@ namespace NadekoBot.Services.Utility | ||||
|                 _log.Warn("Could not load units: " + ex.Message); | ||||
|             } | ||||
|  | ||||
|             _timer = new Timer(async (obj) => await UpdateCurrency(), null, _updateInterval, _updateInterval); | ||||
|             _currencyUpdater = new Timer(async (shouldLoad) => await UpdateCurrency((bool)shouldLoad), client.ShardId == 0, _updateInterval, _updateInterval); | ||||
|         } | ||||
|  | ||||
|         public static async Task<Rates> UpdateCurrencyRates() | ||||
|         private async Task<Rates> GetCurrencyRates() | ||||
|         { | ||||
|             using (var http = new HttpClient()) | ||||
|             { | ||||
| @@ -60,38 +64,48 @@ namespace NadekoBot.Services.Utility | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public async Task UpdateCurrency() | ||||
|         private async Task UpdateCurrency(bool shouldLoad) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 var currencyRates = await UpdateCurrencyRates(); | ||||
|                 var unitTypeString = "currency"; | ||||
|                 var range = currencyRates.ConversionRates.Select(u => new ConvertUnit() | ||||
|                 if (shouldLoad) | ||||
|                 { | ||||
|                     InternalTrigger = u.Key, | ||||
|                     Modifier = u.Value, | ||||
|                     UnitType = unitTypeString | ||||
|                 }).ToArray(); | ||||
|                 var baseType = new ConvertUnit() | ||||
|                 { | ||||
|                     Triggers = new[] { currencyRates.Base }, | ||||
|                     Modifier = decimal.One, | ||||
|                     UnitType = unitTypeString | ||||
|                 }; | ||||
|                 var toRemove = Units.Where(u => u.UnitType == unitTypeString); | ||||
|                     var currencyRates = await GetCurrencyRates(); | ||||
|                     var baseType = new ConvertUnit() | ||||
|                     { | ||||
|                         Triggers = new[] { currencyRates.Base }, | ||||
|                         Modifier = decimal.One, | ||||
|                         UnitType = unitTypeString | ||||
|                     }; | ||||
|                     var range = currencyRates.ConversionRates.Select(u => new ConvertUnit() | ||||
|                     { | ||||
|                         InternalTrigger = u.Key, | ||||
|                         Modifier = u.Value, | ||||
|                         UnitType = unitTypeString | ||||
|                     }).ToArray(); | ||||
|                     var toRemove = Units.Where(u => u.UnitType == unitTypeString); | ||||
|  | ||||
|                 using (var uow = _db.UnitOfWork) | ||||
|                 { | ||||
|                     uow.ConverterUnits.RemoveRange(toRemove.ToArray()); | ||||
|                     uow.ConverterUnits.Add(baseType); | ||||
|                     uow.ConverterUnits.AddRange(range); | ||||
|                     using (var uow = _db.UnitOfWork) | ||||
|                     { | ||||
|                         uow.ConverterUnits.RemoveRange(toRemove.ToArray()); | ||||
|                         uow.ConverterUnits.Add(baseType); | ||||
|                         uow.ConverterUnits.AddRange(range); | ||||
|  | ||||
|                     await uow.CompleteAsync().ConfigureAwait(false); | ||||
|                         await uow.CompleteAsync().ConfigureAwait(false); | ||||
|                     } | ||||
|                     Units.RemoveAll(u => u.UnitType == unitTypeString); | ||||
|                     Units.Add(baseType); | ||||
|                     Units.AddRange(range); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     using (var uow = _db.UnitOfWork) | ||||
|                     { | ||||
|                         Units.RemoveAll(u => u.UnitType == unitTypeString); | ||||
|                         Units.AddRange(uow.ConverterUnits.GetAll().ToArray()); | ||||
|                     } | ||||
|                 } | ||||
|                 Units.RemoveAll(u => u.UnitType == unitTypeString); | ||||
|                 Units.Add(baseType); | ||||
|                 Units.AddRange(range); | ||||
|                 _log.Info("Updated Currency"); | ||||
|             } | ||||
|             catch | ||||
|             { | ||||
|   | ||||
| @@ -1,12 +1,15 @@ | ||||
| using NadekoBot.Services.Database.Models; | ||||
| using Discord.WebSocket; | ||||
| using NadekoBot.Services.Database.Models; | ||||
| using NadekoBot.Services.Utility.Patreon; | ||||
| using Newtonsoft.Json; | ||||
| using NLog; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Net.Http; | ||||
| using System.Text; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| @@ -24,12 +27,13 @@ namespace NadekoBot.Services.Utility | ||||
|         private readonly SemaphoreSlim claimLockJustInCase = new SemaphoreSlim(1, 1); | ||||
|         private readonly Logger _log; | ||||
|  | ||||
|         public readonly TimeSpan Interval = TimeSpan.FromMinutes(15); | ||||
|         private IBotCredentials _creds; | ||||
|         public readonly TimeSpan Interval = TimeSpan.FromMinutes(3); | ||||
|         private readonly IBotCredentials _creds; | ||||
|         private readonly DbService _db; | ||||
|         private readonly CurrencyService _currency; | ||||
|  | ||||
|         public PatreonRewardsService(IBotCredentials creds, DbService db, CurrencyService currency) | ||||
|         public PatreonRewardsService(IBotCredentials creds, DbService db, CurrencyService currency, | ||||
|             DiscordSocketClient client) | ||||
|         { | ||||
|             _creds = creds; | ||||
|             _db = db; | ||||
| @@ -37,58 +41,63 @@ namespace NadekoBot.Services.Utility | ||||
|             if (string.IsNullOrWhiteSpace(creds.PatreonAccessToken)) | ||||
|                 return; | ||||
|             _log = LogManager.GetCurrentClassLogger(); | ||||
|             Updater = new Timer(async (_) => await LoadPledges(), null, TimeSpan.Zero, Interval); | ||||
|             Updater = new Timer(async (load) => await RefreshPledges((bool)load), client.ShardId == 0, TimeSpan.Zero, Interval); | ||||
|         } | ||||
|  | ||||
|         public async Task LoadPledges() | ||||
|         public async Task RefreshPledges(bool shouldLoad) | ||||
|         { | ||||
|             LastUpdate = DateTime.UtcNow; | ||||
|             await getPledgesLocker.WaitAsync(1000).ConfigureAwait(false); | ||||
|             try | ||||
|             if (shouldLoad) | ||||
|             { | ||||
|                 var rewards = new List<PatreonPledge>(); | ||||
|                 var users = new List<PatreonUser>(); | ||||
|                 using (var http = new HttpClient()) | ||||
|                 LastUpdate = DateTime.UtcNow; | ||||
|                 await getPledgesLocker.WaitAsync().ConfigureAwait(false); | ||||
|                 try | ||||
|                 { | ||||
|                     http.DefaultRequestHeaders.Clear(); | ||||
|                     http.DefaultRequestHeaders.Add("Authorization", "Bearer " + _creds.PatreonAccessToken); | ||||
|                     var data = new PatreonData() | ||||
|                     var rewards = new List<PatreonPledge>(); | ||||
|                     var users = new List<PatreonUser>(); | ||||
|                     using (var http = new HttpClient()) | ||||
|                     { | ||||
|                         Links = new PatreonDataLinks() | ||||
|                         http.DefaultRequestHeaders.Clear(); | ||||
|                         http.DefaultRequestHeaders.Add("Authorization", "Bearer " + _creds.PatreonAccessToken); | ||||
|                         var data = new PatreonData() | ||||
|                         { | ||||
|                             next = "https://api.patreon.com/oauth2/api/campaigns/334038/pledges" | ||||
|                         } | ||||
|                     }; | ||||
|                     do | ||||
|                             Links = new PatreonDataLinks() | ||||
|                             { | ||||
|                                 next = "https://api.patreon.com/oauth2/api/campaigns/334038/pledges" | ||||
|                             } | ||||
|                         }; | ||||
|                         do | ||||
|                         { | ||||
|                             var res = await http.GetStringAsync(data.Links.next) | ||||
|                                 .ConfigureAwait(false); | ||||
|                             data = JsonConvert.DeserializeObject<PatreonData>(res); | ||||
|                             var pledgers = data.Data.Where(x => x["type"].ToString() == "pledge"); | ||||
|                             rewards.AddRange(pledgers.Select(x => JsonConvert.DeserializeObject<PatreonPledge>(x.ToString())) | ||||
|                                 .Where(x => x.attributes.declined_since == null)); | ||||
|                             users.AddRange(data.Included | ||||
|                                 .Where(x => x["type"].ToString() == "user") | ||||
|                                 .Select(x => JsonConvert.DeserializeObject<PatreonUser>(x.ToString()))); | ||||
|                         } while (!string.IsNullOrWhiteSpace(data.Links.next)); | ||||
|                     } | ||||
|                     Pledges = rewards.Join(users, (r) => r.relationships?.patron?.data?.id, (u) => u.id, (x, y) => new PatreonUserAndReward() | ||||
|                     { | ||||
|                         var res = await http.GetStringAsync(data.Links.next) | ||||
|                             .ConfigureAwait(false); | ||||
|                         data = JsonConvert.DeserializeObject<PatreonData>(res); | ||||
|                         var pledgers = data.Data.Where(x => x["type"].ToString() == "pledge"); | ||||
|                         rewards.AddRange(pledgers.Select(x => JsonConvert.DeserializeObject<PatreonPledge>(x.ToString())) | ||||
|                             .Where(x => x.attributes.declined_since == null)); | ||||
|                         users.AddRange(data.Included | ||||
|                             .Where(x => x["type"].ToString() == "user") | ||||
|                             .Select(x => JsonConvert.DeserializeObject<PatreonUser>(x.ToString()))); | ||||
|                     } while (!string.IsNullOrWhiteSpace(data.Links.next)); | ||||
|                         User = y, | ||||
|                         Reward = x, | ||||
|                     }).ToImmutableArray(); | ||||
|                     File.WriteAllText("./patreon_rewards.json", JsonConvert.SerializeObject(Pledges)); | ||||
|                 } | ||||
|                 Pledges = rewards.Join(users, (r) => r.relationships?.patron?.data?.id, (u) => u.id, (x, y) => new PatreonUserAndReward() | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     User = y, | ||||
|                     Reward = x, | ||||
|                 }).ToImmutableArray(); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 _log.Warn(ex); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 var _ = Task.Run(async () => | ||||
|                     _log.Warn(ex); | ||||
|                 } | ||||
|                 finally | ||||
|                 { | ||||
|                     await Task.Delay(TimeSpan.FromMinutes(5)).ConfigureAwait(false); | ||||
|                     getPledgesLocker.Release(); | ||||
|                 }); | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 Pledges = JsonConvert.DeserializeObject<PatreonUserAndReward[]>(File.ReadAllText("./patreon_rewards.json")) | ||||
|                     .ToImmutableArray(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user