diff --git a/src/NadekoBot/Common/Attributes/CommandNameLoadHelper.cs b/src/NadekoBot/Common/Attributes/CommandNameLoadHelper.cs index f1aef517b..28b156f8d 100644 --- a/src/NadekoBot/Common/Attributes/CommandNameLoadHelper.cs +++ b/src/NadekoBot/Common/Attributes/CommandNameLoadHelper.cs @@ -6,7 +6,8 @@ public static class CommandNameLoadHelper { private static readonly IDeserializer _deserializer = new Deserializer(); - public static Lazy> LazyCommandAliases = new(() => LoadAliases()); + private static readonly Lazy> _lazyCommandAliases + = new(() => LoadAliases()); public static Dictionary LoadAliases(string aliasesFilePath = "data/aliases.yml") { @@ -15,14 +16,14 @@ public static class CommandNameLoadHelper } public static string[] GetAliasesFor(string methodName) - => LazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1 + => _lazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1 ? aliases.Skip(1).ToArray() : Array.Empty(); public static string GetCommandNameFor(string methodName) { methodName = methodName.ToLowerInvariant(); - var toReturn = LazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0 + var toReturn = _lazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0 ? aliases[0] : methodName; return toReturn; diff --git a/src/NadekoBot/Common/Attributes/OwnerOnlyAttribute.cs b/src/NadekoBot/Common/Attributes/OwnerOnlyAttribute.cs index 13b4dfb7a..872edb9de 100644 --- a/src/NadekoBot/Common/Attributes/OwnerOnlyAttribute.cs +++ b/src/NadekoBot/Common/Attributes/OwnerOnlyAttribute.cs @@ -7,7 +7,7 @@ public sealed class OwnerOnlyAttribute : PreconditionAttribute { public override Task CheckPermissionsAsync( ICommandContext context, - CommandInfo executingCommand, + CommandInfo command, IServiceProvider services) { var creds = services.GetRequiredService().GetCreds(); diff --git a/src/NadekoBot/Common/Collections/ConcurrentHashSet.cs b/src/NadekoBot/Common/Collections/ConcurrentHashSet.cs index 3b2f3261f..4871f9eec 100644 --- a/src/NadekoBot/Common/Collections/ConcurrentHashSet.cs +++ b/src/NadekoBot/Common/Collections/ConcurrentHashSet.cs @@ -1,5 +1,6 @@ #nullable disable -#pragma warning disable all +#pragma warning disable +#pragma warning disable * // License MIT // Source: https://github.com/i3arnon/ConcurrentHashSet @@ -332,16 +333,16 @@ public sealed class ConcurrentHashSet : IReadOnlyCollection, ICollectiontrue if the contains the item; otherwise, false. public bool Contains(T item) { - var hashcode = _comparer.GetHashCode(item); + var hashcode = _comparer.GetHashCode(item!); // We must capture the _buckets field in a local variable. It is set to a new table on each table resize. - var tables = this.tables; + var localTables = this.tables; - var bucketNo = GetBucket(hashcode, tables.Buckets.Length); + var bucketNo = GetBucket(hashcode, localTables.Buckets.Length); // We can get away w/out a lock here. // The Volatile.Read ensures that the load of the fields of 'n' doesn't move before the load from buckets[i]. - var current = Volatile.Read(ref tables.Buckets[bucketNo]); + var current = Volatile.Read(ref localTables.Buckets[bucketNo]); while (current is not null) { @@ -444,29 +445,29 @@ public sealed class ConcurrentHashSet : IReadOnlyCollection, ICollection : IReadOnlyCollection, ICollection : IReadOnlyCollection, ICollection : IReadOnlyCollection, ICollection budget) resizeDesired = true; + if (localTables.CountPerLock[lockNo] > budget) resizeDesired = true; } finally { if (lockTaken) - Monitor.Exit(tables.Locks[lockNo]); + Monitor.Exit(localTables.Locks[lockNo]); } // @@ -542,7 +543,7 @@ public sealed class ConcurrentHashSet : IReadOnlyCollection, ICollection : IReadOnlyCollection, ICollection= 0 && lockNo < lockCount); } - private void GrowTable(Tables tables) + private void GrowTable(Tables localTables) { const int maxArrayLength = 0X7FEFFFFF; var locksAcquired = 0; @@ -579,7 +580,7 @@ public sealed class ConcurrentHashSet : IReadOnlyCollection, ICollection : IReadOnlyCollection, ICollection : IReadOnlyCollection, ICollection : IReadOnlyCollection, ICollection : IList } } - public int IndexOf([NotNull] T item) - => item.Index; + public int IndexOf(T item) + => item?.Index ?? -1; public IEnumerator GetEnumerator() => Source.GetEnumerator(); diff --git a/src/NadekoBot/Common/Creds.cs b/src/NadekoBot/Common/Creds.cs index 78a6a4244..a475646ad 100644 --- a/src/NadekoBot/Common/Creds.cs +++ b/src/NadekoBot/Common/Creds.cs @@ -66,6 +66,10 @@ Used for cryptocurrency related commands.")] [Comment(@"Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api")] public string OsuApiKey { get; set; } + + [Comment(@"Optional Trovo client id. +You should only use this if Trovo notifications stopped working or you're getting ratelimit errors.")] + public string TrovoClientId { get; set; } [Comment(@"Command and args which will be used to restart the bot. Only used if bot is executed directly (NOT through the coordinator) @@ -188,11 +192,11 @@ This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsett 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; } public string ShardRunCommand { get; set; } = string.Empty; public string ShardRunArguments { get; set; } = string.Empty; - public int? ShardRunPort { get; set; } = null; + public int? ShardRunPort { get; set; } public string MiningProxyUrl { get; set; } = string.Empty; public string MiningProxyCreds { get; set; } = string.Empty; diff --git a/src/NadekoBot/Common/IBotCredentials.cs b/src/NadekoBot/Common/IBotCredentials.cs index da1ddeb74..d7be91986 100644 --- a/src/NadekoBot/Common/IBotCredentials.cs +++ b/src/NadekoBot/Common/IBotCredentials.cs @@ -20,6 +20,7 @@ public interface IBotCredentials string LocationIqApiKey { get; } string TimezoneDbApiKey { get; } string CoinmarketcapApiKey { get; } + string TrovoClientId { get; } string CoordinatorUrl { get; set; } } diff --git a/src/NadekoBot/Db/Models/FollowedStream.cs b/src/NadekoBot/Db/Models/FollowedStream.cs index ddf0ed44e..58e2f9472 100644 --- a/src/NadekoBot/Db/Models/FollowedStream.cs +++ b/src/NadekoBot/Db/Models/FollowedStream.cs @@ -11,7 +11,8 @@ public class FollowedStream : DbEntity Twitch = 0, Picarto = 3, Youtube = 4, - Facebook = 5 + Facebook = 5, + Trovo = 6 } public ulong GuildId { get; set; } diff --git a/src/NadekoBot/Modules/Administration/Administration.cs b/src/NadekoBot/Modules/Administration/Administration.cs index 79e7e736d..359c4685a 100644 --- a/src/NadekoBot/Modules/Administration/Administration.cs +++ b/src/NadekoBot/Modules/Administration/Administration.cs @@ -314,12 +314,10 @@ public partial class Administration : NadekoModule } if (time is null) - { await msg.DeleteAsync(); - } else if (time.Time <= TimeSpan.FromDays(7)) { - _= Task.Run(async () => + _ = Task.Run(async () => { await Task.Delay(time.Time); await msg.DeleteAsync(); diff --git a/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs b/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs index b3f34ed59..a6e192f5a 100644 --- a/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs +++ b/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs @@ -39,7 +39,7 @@ public sealed class StreamNotificationService : INService DiscordSocketClient client, IBotStrings strings, ConnectionMultiplexer redis, - IBotCredentials creds, + IBotCredsProvider creds, IHttpClientFactory httpFactory, Bot bot, IPubSub pubSub, @@ -50,7 +50,7 @@ public sealed class StreamNotificationService : INService _strings = strings; _pubSub = pubSub; _eb = eb; - _streamTracker = new(httpFactory, redis, creds.RedisKey(), client.ShardId == 0); + _streamTracker = new(httpFactory, creds, redis, creds.GetCreds().RedisKey(), client.ShardId == 0); _streamsOnlineKey = new("streams.online"); _streamsOfflineKey = new("streams.offline"); diff --git a/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Models/TrovoGetUsersResponse.cs b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Models/TrovoGetUsersResponse.cs new file mode 100644 index 000000000..79da0d01f --- /dev/null +++ b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Models/TrovoGetUsersResponse.cs @@ -0,0 +1,61 @@ +#nullable disable +using System.Text.Json.Serialization; + +namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers; + +public class TrovoGetUsersResponse +{ + [JsonPropertyName("is_live")] + public bool IsLive { get; set; } + + [JsonPropertyName("category_id")] + public string CategoryId { get; set; } + + [JsonPropertyName("category_name")] + public string CategoryName { get; set; } + + [JsonPropertyName("live_title")] + public string LiveTitle { get; set; } + + [JsonPropertyName("audi_type")] + public string AudiType { get; set; } + + [JsonPropertyName("language_code")] + public string LanguageCode { get; set; } + + [JsonPropertyName("thumbnail")] + public string Thumbnail { get; set; } + + [JsonPropertyName("current_viewers")] + public int CurrentViewers { get; set; } + + [JsonPropertyName("followers")] + public int Followers { get; set; } + + [JsonPropertyName("streamer_info")] + public string StreamerInfo { get; set; } + + [JsonPropertyName("profile_pic")] + public string ProfilePic { get; set; } + + [JsonPropertyName("channel_url")] + public string ChannelUrl { get; set; } + + [JsonPropertyName("created_at")] + public string CreatedAt { get; set; } + + [JsonPropertyName("subscriber_num")] + public int SubscriberNum { get; set; } + + [JsonPropertyName("username")] + public string Username { get; set; } + + [JsonPropertyName("social_links")] + public List SocialLinks { get; set; } + + [JsonPropertyName("started_at")] + public string StartedAt { get; set; } + + [JsonPropertyName("ended_at")] + public string EndedAt { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Models/TrovoRequestData.cs b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Models/TrovoRequestData.cs new file mode 100644 index 000000000..def9b2d11 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Models/TrovoRequestData.cs @@ -0,0 +1,10 @@ +#nullable disable +using System.Text.Json.Serialization; + +namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers; + +public class TrovoRequestData +{ + [JsonPropertyName("channel_id")] + public string ChannelId { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Models/TrovoSocialLink.cs b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Models/TrovoSocialLink.cs new file mode 100644 index 000000000..00ccee77e --- /dev/null +++ b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Models/TrovoSocialLink.cs @@ -0,0 +1,13 @@ +#nullable disable +using System.Text.Json.Serialization; + +namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers; + +public class TrovoSocialLink +{ + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/NotifChecker.cs b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/NotifChecker.cs index db006d172..31cc473c2 100644 --- a/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/NotifChecker.cs +++ b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/NotifChecker.cs @@ -17,6 +17,7 @@ public class NotifChecker public NotifChecker( IHttpClientFactory httpClientFactory, + IBotCredsProvider credsProvider, ConnectionMultiplexer multi, string uniqueCacheKey, bool isMaster) @@ -26,7 +27,8 @@ public class NotifChecker _streamProviders = new() { { FollowedStream.FType.Twitch, new TwitchProvider(httpClientFactory) }, - { FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) } + { FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) }, + { FollowedStream.FType.Trovo, new TrovoProvider(httpClientFactory, credsProvider) } }; _offlineBuffer = new(); if (isMaster) CacheClearAllData(); @@ -35,17 +37,20 @@ public class NotifChecker // gets all streams which have been failing for more than the provided timespan public IEnumerable GetFailingStreams(TimeSpan duration, bool remove = false) { - var toReturn = _streamProviders.SelectMany(prov => prov.Value - .FailingStreams - .Where(fs => DateTime.UtcNow - fs.ErroringSince - > duration) - .Select(fs => new StreamDataKey(prov.Value.Platform, - fs.Item1))) - .ToList(); + var toReturn = _streamProviders + .SelectMany(prov => prov.Value + .FailingStreams + .Where(fs => DateTime.UtcNow - fs.Value > duration) + .Select(fs => new StreamDataKey(prov.Value.Platform, fs.Key))) + .ToList(); if (remove) + { foreach (var toBeRemoved in toReturn) + { _streamProviders[toBeRemoved.Type].ClearErrorsFor(toBeRemoved.Name); + } + } return toReturn; } @@ -54,6 +59,7 @@ public class NotifChecker => Task.Run(async () => { while (true) + { try { var allStreamData = CacheGetAllData(); @@ -65,19 +71,21 @@ public class NotifChecker entry => entry.AsEnumerable() .ToDictionary(x => x.Key.Name, x => x.Value)); - var newStreamData = await oldStreamDataDict.Select(x => - { - // get all stream data for the streams of this type - if (_streamProviders.TryGetValue(x.Key, - out var provider)) - return provider.GetStreamDataAsync(x.Value - .Select(entry => entry.Key) - .ToList()); + var newStreamData = await oldStreamDataDict + .Select(x => + { + // get all stream data for the streams of this type + if (_streamProviders.TryGetValue(x.Key, + out var provider)) + return provider.GetStreamDataAsync(x.Value + .Select(entry => entry.Key) + .ToList()); - // this means there's no provider for this stream data, (and there was before?) - return Task.FromResult(new List()); - }) - .WhenAll(); + // this means there's no provider for this stream data, (and there was before?) + return Task.FromResult>( + new List()); + }) + .WhenAll(); var newlyOnline = new List(); var newlyOffline = new List(); @@ -124,7 +132,10 @@ public class NotifChecker } } - var tasks = new List { Task.Delay(30_000) }; + var tasks = new List + { + Task.Delay(30_000) + }; if (newlyOnline.Count > 0) tasks.Add(OnStreamsOnline(newlyOnline)); @@ -136,6 +147,7 @@ public class NotifChecker { Log.Error(ex, "Error getting stream notifications: {ErrorMessage}", ex.Message); } + } }); public bool CacheAddData(StreamDataKey key, StreamData? data, bool replace) diff --git a/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/PicartoProvider.cs b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/PicartoProvider.cs index 0627bb9b8..cdf178b87 100644 --- a/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/PicartoProvider.cs +++ b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/PicartoProvider.cs @@ -46,10 +46,10 @@ public class PicartoProvider : Provider return data.FirstOrDefault(); } - public override async Task> GetStreamDataAsync(List logins) + public override async Task> GetStreamDataAsync(List logins) { if (logins.Count == 0) - return new(); + return new List(); using var http = _httpClientFactory.CreateClient(); var toReturn = new List(); diff --git a/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/Provider.cs b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/Provider.cs index dc05a246f..18af358b1 100644 --- a/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/Provider.cs +++ b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/Provider.cs @@ -15,9 +15,10 @@ public abstract class Provider /// /// Gets the stream usernames which fail to execute due to an error, and when they started throwing errors. /// This can happen if stream name is invalid, or if the stream doesn't exist anymore. + /// Override to provide a custom implementation /// - public IEnumerable<(string Login, DateTime ErroringSince)> FailingStreams - => _failingStreams.Select(entry => (entry.Key, entry.Value)).ToList(); + public virtual IReadOnlyDictionary FailingStreams + => _failingStreams; /// /// When was the first time the stream continually had errors while being retrieved @@ -50,8 +51,13 @@ public abstract class Provider /// /// List of ids/usernames /// of all users, in the same order. Null for every id/user not found. - public abstract Task> GetStreamDataAsync(List usernames); + public abstract Task> GetStreamDataAsync(List usernames); - public void ClearErrorsFor(string login) - => _failingStreams.TryRemove(login, out _); + /// + /// Unmark the stream as errored. You should override this method + /// if you've overridden the property. + /// + /// + public virtual void ClearErrorsFor(string login) + => _failingStreams.Clear(); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/TrovoProvider.cs b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/TrovoProvider.cs new file mode 100644 index 000000000..c648d5dd1 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/TrovoProvider.cs @@ -0,0 +1,106 @@ +using NadekoBot.Db.Models; +using System.Net.Http.Json; +using System.Text.RegularExpressions; + +namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers; + +public class TrovoProvider : Provider +{ + private readonly IHttpClientFactory _httpClientFactory; + + public override FollowedStream.FType Platform + => FollowedStream.FType.Trovo; + + private readonly Regex _urlRegex + = new Regex(@"trovo.live\/(?[\w\d\-_]+)/?", RegexOptions.Compiled); + + private readonly IBotCredsProvider _creds; + + + public TrovoProvider(IHttpClientFactory httpClientFactory, IBotCredsProvider creds) + => (_httpClientFactory, _creds) = (httpClientFactory, creds); + + public override Task IsValidUrl(string url) + => Task.FromResult(_urlRegex.IsMatch(url)); + + public override Task GetStreamDataByUrlAsync(string url) + { + var match = _urlRegex.Match(url); + if (match.Length == 0) + return Task.FromResult(default(StreamData?)); + + return GetStreamDataAsync(match.Groups["channel"].Value); + } + + public override async Task GetStreamDataAsync(string id) + { + using var http = _httpClientFactory.CreateClient(); + + var trovoClientId = _creds.GetCreds().TrovoClientId; + + if (string.IsNullOrWhiteSpace(trovoClientId)) + trovoClientId = "waiting for key"; + + + http.DefaultRequestHeaders.Clear(); + http.DefaultRequestHeaders.Add("Accept", "application/json"); + http.DefaultRequestHeaders.Add("Client-ID", trovoClientId); + + // trovo ratelimit is very generous (1200 per minute) + // so there is no need for ratelimit checks atm + try + { + var res = await http.PostAsJsonAsync( + $"https://open-api.trovo.live/openplatform/channels/id", + new TrovoRequestData() + { + ChannelId = id + }); + + res.EnsureSuccessStatusCode(); + + var data = await res.Content.ReadFromJsonAsync(); + + if (data is null) + { + Log.Warning("An empty response received while retrieving stream data for trovo.live/{TrovoId}", id); + _failingStreams.TryAdd(id, DateTime.UtcNow); + return null; + } + + return new() + { + IsLive = data.IsLive, + Game = data.CategoryName, + Name = data.Username, + Title = data.LiveTitle, + Viewers = data.CurrentViewers, + AvatarUrl = data.ProfilePic, + StreamType = FollowedStream.FType.Picarto, + StreamUrl = data.ChannelUrl, + UniqueName = data.Username, + Preview = data.Thumbnail, + }; + } + catch (Exception ex) + { + Log.Warning(ex, "Error retrieving stream data for trovo.live/{TrovoId}", id); + _failingStreams.TryAdd(id, DateTime.UtcNow); + return null; + } + } + + public override async Task> GetStreamDataAsync(List usernames) + { + var results = new List(usernames.Count); + foreach (var chunk in usernames.Chunk(10) + .Select(x => x.Select(GetStreamDataAsync))) + { + var chunkResults = await Task.WhenAll(chunk); + results.AddRange(chunkResults.Where(x => x is not null)!); + await Task.Delay(1000); + } + + return results; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/TwitchProvider.cs b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/TwitchProvider.cs index deffae666..226dcb73d 100644 --- a/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/TwitchProvider.cs +++ b/src/NadekoBot/Modules/Searches/_Common/StreamNotifications/Providers/TwitchProvider.cs @@ -12,11 +12,17 @@ public class TwitchProvider : Provider public override FollowedStream.FType Platform => FollowedStream.FType.Twitch; + public override IReadOnlyDictionary FailingStreams + => _failingStreams; + private readonly IHttpClientFactory _httpClientFactory; public TwitchProvider(IHttpClientFactory httpClientFactory) => _httpClientFactory = httpClientFactory; + public override void ClearErrorsFor(string login) + => _failingStreams.TryRemove(login, out _); + public override Task IsValidUrl(string url) { var match = Regex.Match(url); @@ -41,15 +47,18 @@ public class TwitchProvider : Provider public override async Task GetStreamDataAsync(string id) { - var data = await GetStreamDataAsync(new List { id }); + var data = await GetStreamDataAsync(new List + { + id + }); return data.FirstOrDefault(); } - public override async Task> GetStreamDataAsync(List logins) + public override async Task> GetStreamDataAsync(List logins) { if (logins.Count == 0) - return new(); + return new List(); using var http = _httpClientFactory.CreateClient(); http.DefaultRequestHeaders.Add("Client-Id", "67w6z9i09xv2uoojdm9l0wsyph4hxo6"); @@ -70,7 +79,11 @@ public class TwitchProvider : Provider // get stream data var str = await http.GetStringAsync($"https://api.twitch.tv/kraken/streams/{user.Id}"); - var resObj = JsonConvert.DeserializeAnonymousType(str, new { Stream = new TwitchResponseV5.Stream() }); + var resObj = JsonConvert.DeserializeAnonymousType(str, + new + { + Stream = new TwitchResponseV5.Stream() + }); // if stream is null, user is not streaming if (resObj?.Stream is null) diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 3b7a33f30..553053d29 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -9,7 +9,8 @@ $(MSBuildProjectDirectory) exe nadeko_icon.ico - CS1066 + CS1066,CA1069 + Recommended diff --git a/src/NadekoBot/Services/Impl/BotCredsProvider.cs b/src/NadekoBot/Services/Impl/BotCredsProvider.cs index 35be5b293..2abd7ea1a 100644 --- a/src/NadekoBot/Services/Impl/BotCredsProvider.cs +++ b/src/NadekoBot/Services/Impl/BotCredsProvider.cs @@ -119,6 +119,12 @@ public sealed class BotCredsProvider : IBotCredsProvider var jsonCredentialsFileText = File.ReadAllText(OldCredsJsonPath); var oldCreds = JsonConvert.DeserializeObject(jsonCredentialsFileText); + if (oldCreds is null) + { + Log.Error("Error while reading old credentials file. Make sure that the file is formatted correctly"); + return; + } + var creds = new Creds { Version = 1,