mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-03 16:24:27 -05:00 
			
		
		
		
	- NadekoBot class renamed to Bot
- Implemented grpc based coordinator. Supports restarting, killing single or all shards, as well as getting current shard statuses. (Adaptation of the one used by the public bot) - Coord is setup via coord.yml file - Methods from SelfService which deal with shard/bot restart etc have been moved to ICoordinator (with GrpcRemoteCoordinator being the default implementation atm) - Vastly simplified NadekoBot/Program.cs
This commit is contained in:
		@@ -3,7 +3,6 @@ using Discord.Commands;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.ShardCom;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Core.Services.Impl;
 | 
			
		||||
@@ -26,16 +25,16 @@ using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using NadekoBot.Core.Common.Configs;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Modules.Administration;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using NadekoBot.Modules.CustomReactions.Services;
 | 
			
		||||
using NadekoBot.Modules.Utility.Services;
 | 
			
		||||
using Serilog;
 | 
			
		||||
using NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot
 | 
			
		||||
{
 | 
			
		||||
    public class NadekoBot
 | 
			
		||||
    public class Bot
 | 
			
		||||
    {
 | 
			
		||||
        public BotCredentials Credentials { get; }
 | 
			
		||||
        public DiscordSocketClient Client { get; }
 | 
			
		||||
@@ -54,22 +53,15 @@ namespace NadekoBot
 | 
			
		||||
        public IServiceProvider Services { get; private set; }
 | 
			
		||||
        public IDataCache Cache { get; private set; }
 | 
			
		||||
 | 
			
		||||
        public int GuildCount =>
 | 
			
		||||
            Cache.Redis.GetDatabase()
 | 
			
		||||
                .ListRange(Credentials.RedisKey() + "_shardstats")
 | 
			
		||||
                .Select(x => JsonConvert.DeserializeObject<ShardComMessage>(x))
 | 
			
		||||
                .Sum(x => x.Guilds);
 | 
			
		||||
 | 
			
		||||
        public string Mention { get; set; }
 | 
			
		||||
 | 
			
		||||
        public event Func<GuildConfig, Task> JoinedGuild = delegate { return Task.CompletedTask; };
 | 
			
		||||
 | 
			
		||||
        public NadekoBot(int shardId, int parentProcessId)
 | 
			
		||||
        public Bot(int shardId)
 | 
			
		||||
        {
 | 
			
		||||
            if (shardId < 0)
 | 
			
		||||
                throw new ArgumentOutOfRangeException(nameof(shardId));
 | 
			
		||||
 | 
			
		||||
            LogSetup.SetupLogger(shardId);
 | 
			
		||||
            
 | 
			
		||||
            TerribleElevatedPermissionCheck();
 | 
			
		||||
 | 
			
		||||
            Credentials = new BotCredentials();
 | 
			
		||||
@@ -99,36 +91,11 @@ namespace NadekoBot
 | 
			
		||||
                DefaultRunMode = RunMode.Sync,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            SetupShard(parentProcessId);
 | 
			
		||||
 | 
			
		||||
#if GLOBAL_NADEKO || DEBUG
 | 
			
		||||
            Client.Log += Client_Log;
 | 
			
		||||
#endif
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void StartSendingData()
 | 
			
		||||
        {
 | 
			
		||||
            Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                while (true)
 | 
			
		||||
                {
 | 
			
		||||
                    var data = new ShardComMessage()
 | 
			
		||||
                    {
 | 
			
		||||
                        ConnectionState = Client.ConnectionState,
 | 
			
		||||
                        Guilds = Client.ConnectionState == ConnectionState.Connected ? Client.Guilds.Count : 0,
 | 
			
		||||
                        ShardId = Client.ShardId,
 | 
			
		||||
                        Time = DateTime.UtcNow,
 | 
			
		||||
                    };
 | 
			
		||||
 | 
			
		||||
                    var sub = Cache.Redis.GetSubscriber();
 | 
			
		||||
                    var msg = JsonConvert.SerializeObject(data);
 | 
			
		||||
 | 
			
		||||
                    await sub.PublishAsync(Credentials.RedisKey() + "_shardcoord_send", msg).ConfigureAwait(false);
 | 
			
		||||
                    await Task.Delay(7500).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public List<ulong> GetCurrentGuildIds()
 | 
			
		||||
        {
 | 
			
		||||
            return Client.Guilds.Select(x => x.Id).ToList();
 | 
			
		||||
@@ -180,9 +147,15 @@ namespace NadekoBot
 | 
			
		||||
 | 
			
		||||
            s.LoadFrom(Assembly.GetAssembly(typeof(CommandHandler)));
 | 
			
		||||
 | 
			
		||||
            // todo if sharded
 | 
			
		||||
            s
 | 
			
		||||
             .AddSingleton<ICoordinator, RemoteGrpcCoordinator>()
 | 
			
		||||
             .AddSingleton<IReadyExecutor>(x => (IReadyExecutor)x.GetRequiredService<ICoordinator>());
 | 
			
		||||
 | 
			
		||||
            s.AddSingleton<IReadyExecutor>(x => x.GetService<SelfService>());
 | 
			
		||||
            s.AddSingleton<IReadyExecutor>(x => x.GetService<CustomReactionsService>());
 | 
			
		||||
            s.AddSingleton<IReadyExecutor>(x => x.GetService<RepeaterService>());
 | 
			
		||||
 | 
			
		||||
            //initialize Services
 | 
			
		||||
            Services = s.BuildServiceProvider();
 | 
			
		||||
            var commandHandler = Services.GetService<CommandHandler>();
 | 
			
		||||
@@ -194,7 +167,7 @@ namespace NadekoBot
 | 
			
		||||
 | 
			
		||||
            //what the fluff
 | 
			
		||||
            commandHandler.AddServices(s);
 | 
			
		||||
            _ = LoadTypeReaders(typeof(NadekoBot).Assembly);
 | 
			
		||||
            _ = LoadTypeReaders(typeof(Bot).Assembly);
 | 
			
		||||
 | 
			
		||||
            sw.Stop();
 | 
			
		||||
            Log.Information($"All services loaded in {sw.Elapsed.TotalSeconds:F2}s");
 | 
			
		||||
@@ -355,7 +328,6 @@ namespace NadekoBot
 | 
			
		||||
                .ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            HandleStatusChanges();
 | 
			
		||||
            StartSendingData();
 | 
			
		||||
            Ready.TrySetResult(true);
 | 
			
		||||
            _ = Task.Run(ExecuteReadySubscriptions);
 | 
			
		||||
            Log.Information("Shard {ShardId} ready", Client.ShardId);
 | 
			
		||||
@@ -412,22 +384,6 @@ namespace NadekoBot
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private static void SetupShard(int parentProcessId)
 | 
			
		||||
        {
 | 
			
		||||
            new Thread(new ThreadStart(() =>
 | 
			
		||||
            {
 | 
			
		||||
                try
 | 
			
		||||
                {
 | 
			
		||||
                    var p = Process.GetProcessById(parentProcessId);
 | 
			
		||||
                    p.WaitForExit();
 | 
			
		||||
                }
 | 
			
		||||
                finally
 | 
			
		||||
                {
 | 
			
		||||
                    Environment.Exit(7);
 | 
			
		||||
                }
 | 
			
		||||
            })).Start();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void HandleStatusChanges()
 | 
			
		||||
        {
 | 
			
		||||
            var sub = Services.GetService<IDataCache>().Redis.GetSubscriber();
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Discord;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common.ShardCom
 | 
			
		||||
{
 | 
			
		||||
    public class ShardComMessage
 | 
			
		||||
    {
 | 
			
		||||
        public int ShardId { get; set; }
 | 
			
		||||
        public ConnectionState ConnectionState { get; set; }
 | 
			
		||||
        public int Guilds { get; set; }
 | 
			
		||||
        public DateTime Time { get; set; }
 | 
			
		||||
 | 
			
		||||
        public ShardComMessage Clone() =>
 | 
			
		||||
            new ShardComMessage
 | 
			
		||||
            {
 | 
			
		||||
                ShardId = ShardId,
 | 
			
		||||
                ConnectionState = ConnectionState,
 | 
			
		||||
                Guilds = Guilds,
 | 
			
		||||
                Time = Time,
 | 
			
		||||
            };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Common.ShardCom
 | 
			
		||||
{
 | 
			
		||||
    public class ShardComServer
 | 
			
		||||
    {
 | 
			
		||||
        private readonly IDataCache _cache;
 | 
			
		||||
 | 
			
		||||
        public ShardComServer(IDataCache cache)
 | 
			
		||||
        {
 | 
			
		||||
            _cache = cache;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Start()
 | 
			
		||||
        {
 | 
			
		||||
            var sub = _cache.Redis.GetSubscriber();
 | 
			
		||||
            sub.SubscribeAsync("shardcoord_send", (ch, data) =>
 | 
			
		||||
            {
 | 
			
		||||
                var _ = OnDataReceived(JsonConvert.DeserializeObject<ShardComMessage>(data));
 | 
			
		||||
            }, StackExchange.Redis.CommandFlags.FireAndForget);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public event Func<ShardComMessage, Task> OnDataReceived = delegate { return Task.CompletedTask; };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +1,9 @@
 | 
			
		||||
 | 
			
		||||
using System;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Core.Services.Database;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Migrations;
 | 
			
		||||
using NadekoBot.Db.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Db
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ using System;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Services;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration
 | 
			
		||||
@@ -22,14 +23,16 @@ namespace NadekoBot.Modules.Administration
 | 
			
		||||
        public class SelfCommands : NadekoSubmodule<SelfService>
 | 
			
		||||
        {
 | 
			
		||||
            private readonly DiscordSocketClient _client;
 | 
			
		||||
            private readonly NadekoBot _bot;
 | 
			
		||||
            private readonly Bot _bot;
 | 
			
		||||
            private readonly IBotStrings _strings;
 | 
			
		||||
            private readonly ICoordinator _coord;
 | 
			
		||||
 | 
			
		||||
            public SelfCommands(DiscordSocketClient client, NadekoBot bot, IBotStrings strings)
 | 
			
		||||
            public SelfCommands(DiscordSocketClient client, Bot bot, IBotStrings strings, ICoordinator coord)
 | 
			
		||||
            {
 | 
			
		||||
                _client = client;
 | 
			
		||||
                _bot = bot;
 | 
			
		||||
                _strings = strings;
 | 
			
		||||
                _coord = coord;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
@@ -251,7 +254,7 @@ namespace NadekoBot.Modules.Administration
 | 
			
		||||
                if (--page < 0)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                var statuses = _service.GetAllShardStatuses();
 | 
			
		||||
                var statuses = _coord.GetAllShardStatuses();
 | 
			
		||||
 | 
			
		||||
                var status = string.Join(", ", statuses
 | 
			
		||||
                    .GroupBy(x => x.ConnectionState)
 | 
			
		||||
@@ -289,7 +292,7 @@ namespace NadekoBot.Modules.Administration
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task RestartShard(int shardId)
 | 
			
		||||
            {
 | 
			
		||||
                var success = _service.RestartShard(shardId);
 | 
			
		||||
                var success = _coord.RestartShard(shardId);
 | 
			
		||||
                if (success)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyConfirmLocalizedAsync("shard_reconnecting", Format.Bold("#" + shardId)).ConfigureAwait(false);
 | 
			
		||||
@@ -321,14 +324,14 @@ namespace NadekoBot.Modules.Administration
 | 
			
		||||
                    // ignored
 | 
			
		||||
                }
 | 
			
		||||
                await Task.Delay(2000).ConfigureAwait(false);
 | 
			
		||||
                _service.Die();
 | 
			
		||||
                _coord.Die();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
            [OwnerOnly]
 | 
			
		||||
            public async Task Restart()
 | 
			
		||||
            {
 | 
			
		||||
                bool success = _service.RestartBot();
 | 
			
		||||
                bool success = _coord.RestartBot();
 | 
			
		||||
                if (!success)
 | 
			
		||||
                {
 | 
			
		||||
                    await ReplyErrorLocalizedAsync("restart_fail").ConfigureAwait(false);
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly LogCommandService _logService;
 | 
			
		||||
 | 
			
		||||
        public AdministrationService(NadekoBot bot, CommandHandler cmdHandler, DbService db,
 | 
			
		||||
        public AdministrationService(Bot bot, CommandHandler cmdHandler, DbService db,
 | 
			
		||||
            LogCommandService logService)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@ namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
                SingleWriter = false,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        public AutoAssignRoleService(DiscordSocketClient client, NadekoBot bot, DbService db)
 | 
			
		||||
        public AutoAssignRoleService(DiscordSocketClient client, Bot bot, DbService db)
 | 
			
		||||
        {
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _db = db;
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
 | 
			
		||||
        public GameVoiceChannelService(DiscordSocketClient client, DbService db, NadekoBot bot)
 | 
			
		||||
        public GameVoiceChannelService(DiscordSocketClient client, DbService db, Bot bot)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _client = client;
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, TimeZoneInfo> _timezones;
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
        public GuildTimezoneService(DiscordSocketClient client, NadekoBot bot, DbService db)
 | 
			
		||||
        public GuildTimezoneService(DiscordSocketClient client, Bot bot, DbService db)
 | 
			
		||||
        {
 | 
			
		||||
            _timezones = bot.AllGuildConfigs
 | 
			
		||||
                .Select(GetTimzezoneTuple)
 | 
			
		||||
 
 | 
			
		||||
@@ -20,14 +20,14 @@ namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
        private readonly BotConfigService _bss;
 | 
			
		||||
        private readonly Replacer _rep;
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly NadekoBot _bot;
 | 
			
		||||
        private readonly Bot _bot;
 | 
			
		||||
 | 
			
		||||
        private class TimerState
 | 
			
		||||
        {
 | 
			
		||||
            public int Index { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public PlayingRotateService(DiscordSocketClient client, DbService db, NadekoBot bot,
 | 
			
		||||
        public PlayingRotateService(DiscordSocketClient client, DbService db, Bot bot,
 | 
			
		||||
            BotConfigService bss, IEnumerable<IPlaceholderProvider> phProviders)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
                SingleWriter = false
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        public ProtectionService(DiscordSocketClient client, NadekoBot bot,
 | 
			
		||||
        public ProtectionService(DiscordSocketClient client, Bot bot,
 | 
			
		||||
            MuteService mute, DbService db, UserPunishService punishService)
 | 
			
		||||
        { 
 | 
			
		||||
            _client = client;
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, IndexedCollection<ReactionRoleMessage>> _models;
 | 
			
		||||
 | 
			
		||||
        public RoleCommandsService(DiscordSocketClient client, DbService db,
 | 
			
		||||
            NadekoBot bot)
 | 
			
		||||
            Bot bot)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _client = client;
 | 
			
		||||
 
 | 
			
		||||
@@ -9,14 +9,13 @@ using NadekoBot.Core.Services;
 | 
			
		||||
using StackExchange.Redis;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using NadekoBot.Common.ShardCom;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Core.Services.Database.Models;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Collections.Concurrent;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Net.Http;
 | 
			
		||||
using NadekoBot.Services;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
@@ -41,10 +40,11 @@ namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
        private readonly IImageCache _imgs;
 | 
			
		||||
        private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
        private readonly BotConfigService _bss;
 | 
			
		||||
        private readonly ICoordinator _coord;
 | 
			
		||||
 | 
			
		||||
        public SelfService(DiscordSocketClient client, CommandHandler cmdHandler, DbService db,
 | 
			
		||||
            IBotStrings strings, IBotCredentials creds, IDataCache cache, IHttpClientFactory factory,
 | 
			
		||||
            BotConfigService bss)
 | 
			
		||||
            BotConfigService bss, ICoordinator coord)
 | 
			
		||||
        {
 | 
			
		||||
            _redis = cache.Redis;
 | 
			
		||||
            _cmdHandler = cmdHandler;
 | 
			
		||||
@@ -56,6 +56,7 @@ namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
            _imgs = cache.LocalImages;
 | 
			
		||||
            _httpFactory = factory;
 | 
			
		||||
            _bss = bss;
 | 
			
		||||
            _coord = coord;
 | 
			
		||||
 | 
			
		||||
            var sub = _redis.GetSubscriber();
 | 
			
		||||
            if (_client.ShardId == 0)
 | 
			
		||||
@@ -281,18 +282,6 @@ namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool RestartBot()
 | 
			
		||||
        {
 | 
			
		||||
            var cmd = _creds.RestartCommand;
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(cmd?.Cmd))
 | 
			
		||||
            {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Restart();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool RemoveStartupCommand(int index, out AutoCommand cmd)
 | 
			
		||||
        {
 | 
			
		||||
            using (var uow = _db.GetDbContext())
 | 
			
		||||
@@ -385,32 +374,6 @@ namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
            sub.Publish(_creds.RedisKey() + "_reload_images", "");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Die()
 | 
			
		||||
        {
 | 
			
		||||
            var sub = _cache.Redis.GetSubscriber();
 | 
			
		||||
            sub.Publish(_creds.RedisKey() + "_die", "", CommandFlags.FireAndForget);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Restart()
 | 
			
		||||
        {
 | 
			
		||||
            Process.Start(_creds.RestartCommand.Cmd, _creds.RestartCommand.Args);
 | 
			
		||||
            var sub = _cache.Redis.GetSubscriber();
 | 
			
		||||
            sub.Publish(_creds.RedisKey() + "_die", "", CommandFlags.FireAndForget);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool RestartShard(int shardId)
 | 
			
		||||
        {
 | 
			
		||||
            if (shardId < 0 || shardId >= _creds.TotalShards)
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            var pub = _cache.Redis.GetSubscriber();
 | 
			
		||||
            pub.Publish(_creds.RedisKey() + "_shardcoord_stop",
 | 
			
		||||
                JsonConvert.SerializeObject(shardId),
 | 
			
		||||
                CommandFlags.FireAndForget);
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool ForwardMessages()
 | 
			
		||||
        {
 | 
			
		||||
            var isForwarding = false;
 | 
			
		||||
@@ -425,12 +388,5 @@ namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
            _bss.ModifyConfig(config => { isToAll = config.ForwardToAllOwners = !config.ForwardToAllOwners; });
 | 
			
		||||
            return isToAll;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IEnumerable<ShardComMessage> GetAllShardStatuses()
 | 
			
		||||
        {
 | 
			
		||||
            var db = _cache.Redis.GetDatabase();
 | 
			
		||||
            return db.ListRange(_creds.RedisKey() + "_shardstats")
 | 
			
		||||
                .Select(x => JsonConvert.DeserializeObject<ShardComMessage>(x));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -456,7 +456,7 @@ WHERE GuildId={guildId}
 | 
			
		||||
            {
 | 
			
		||||
                template = JsonConvert.SerializeObject(new
 | 
			
		||||
                {
 | 
			
		||||
                    color = NadekoBot.ErrorColor.RawValue,
 | 
			
		||||
                    color = Bot.ErrorColor.RawValue,
 | 
			
		||||
                    description = defaultMessage 
 | 
			
		||||
                });
 | 
			
		||||
                
 | 
			
		||||
@@ -477,7 +477,7 @@ WHERE GuildId={guildId}
 | 
			
		||||
            {
 | 
			
		||||
                template = JsonConvert.SerializeObject(new
 | 
			
		||||
                {
 | 
			
		||||
                    color = NadekoBot.ErrorColor.RawValue,
 | 
			
		||||
                    color = Bot.ErrorColor.RawValue,
 | 
			
		||||
                    description = replacer.Replace(template) 
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ namespace NadekoBot.Modules.Administration.Services
 | 
			
		||||
        public ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, IRole>> VcRoles { get; }
 | 
			
		||||
        public ConcurrentDictionary<ulong, ConcurrentQueue<(bool, IGuildUser, IRole)>> ToAssign { get; }
 | 
			
		||||
 | 
			
		||||
        public VcRoleService(DiscordSocketClient client, NadekoBot bot, DbService db)
 | 
			
		||||
        public VcRoleService(DiscordSocketClient client, Bot bot, DbService db)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _client = client;
 | 
			
		||||
 
 | 
			
		||||
@@ -57,13 +57,13 @@ namespace NadekoBot.Modules.CustomReactions.Services
 | 
			
		||||
        private readonly PermissionService _perms;
 | 
			
		||||
        private readonly CommandHandler _cmd;
 | 
			
		||||
        private readonly IBotStrings _strings;
 | 
			
		||||
        private readonly NadekoBot _bot;
 | 
			
		||||
        private readonly Bot _bot;
 | 
			
		||||
        private readonly GlobalPermissionService _gperm;
 | 
			
		||||
        private readonly CmdCdService _cmdCds;
 | 
			
		||||
        private readonly IPubSub _pubSub;
 | 
			
		||||
        private readonly Random _rng;
 | 
			
		||||
 | 
			
		||||
        public CustomReactionsService(PermissionService perms, DbService db, IBotStrings strings, NadekoBot bot,
 | 
			
		||||
        public CustomReactionsService(PermissionService perms, DbService db, IBotStrings strings, Bot bot,
 | 
			
		||||
            DiscordSocketClient client, CommandHandler cmd, GlobalPermissionService gperm, CmdCdService cmdCds,
 | 
			
		||||
            IPubSub pubSub)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ namespace NadekoBot.Modules.Gambling.Services
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly ICurrencyService _cs;
 | 
			
		||||
        private readonly NadekoBot _bot;
 | 
			
		||||
        private readonly Bot _bot;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly IDataCache _cache;
 | 
			
		||||
        private readonly GamblingConfigService _gss;
 | 
			
		||||
@@ -31,7 +31,7 @@ namespace NadekoBot.Modules.Gambling.Services
 | 
			
		||||
 | 
			
		||||
        private readonly Timer _decayTimer;
 | 
			
		||||
 | 
			
		||||
        public GamblingService(DbService db, NadekoBot bot, ICurrencyService cs,
 | 
			
		||||
        public GamblingService(DbService db, Bot bot, ICurrencyService cs,
 | 
			
		||||
            DiscordSocketClient client, IDataCache cache, GamblingConfigService gss)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@ namespace NadekoBot.Modules.Games
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var res = _service.GetEightballResponse(ctx.User.Id, question);
 | 
			
		||||
            await ctx.Channel.EmbedAsync(new EmbedBuilder().WithColor(NadekoBot.OkColor)
 | 
			
		||||
            await ctx.Channel.EmbedAsync(new EmbedBuilder().WithColor(Bot.OkColor)
 | 
			
		||||
                .WithDescription(ctx.User.ToString())
 | 
			
		||||
                .AddField(efb => efb.WithName("❓ " + GetText("question")).WithValue(question).WithIsInline(false))
 | 
			
		||||
                .AddField("🎱 " + GetText("8ball"), res, false));
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ namespace NadekoBot.Modules.Games.Services
 | 
			
		||||
        public ModuleBehaviorType BehaviorType => ModuleBehaviorType.Executor;
 | 
			
		||||
 | 
			
		||||
        public ChatterBotService(DiscordSocketClient client, PermissionService perms,
 | 
			
		||||
            NadekoBot bot, CommandHandler cmd, IBotStrings strings, IHttpClientFactory factory,
 | 
			
		||||
            Bot bot, CommandHandler cmd, IBotStrings strings, IHttpClientFactory factory,
 | 
			
		||||
            IBotCredentials creds)
 | 
			
		||||
        {
 | 
			
		||||
            _client = client;
 | 
			
		||||
 
 | 
			
		||||
@@ -83,7 +83,7 @@ namespace NadekoBot.Modules.Help.Services
 | 
			
		||||
                        arg => Format.Code(arg))))
 | 
			
		||||
                    .WithIsInline(false))
 | 
			
		||||
                .WithFooter(efb => efb.WithText(GetText("module", guild, com.Module.GetTopLevelModule().Name)))
 | 
			
		||||
                .WithColor(NadekoBot.OkColor);
 | 
			
		||||
                .WithColor(Bot.OkColor);
 | 
			
		||||
 | 
			
		||||
            var opt = ((NadekoOptionsAttribute)com.Attributes.FirstOrDefault(x => x is NadekoOptionsAttribute))?.OptionType;
 | 
			
		||||
            if (opt != null)
 | 
			
		||||
 
 | 
			
		||||
@@ -619,7 +619,7 @@ namespace NadekoBot.Modules.Music
 | 
			
		||||
                 .WithAuthor(eab => eab.WithName(GetText("song_moved")).WithIconUrl("https://cdn.discordapp.com/attachments/155726317222887425/258605269972549642/music1.png"))
 | 
			
		||||
                 .AddField(fb => fb.WithName(GetText("from_position")).WithValue($"#{from + 1}").WithIsInline(true))
 | 
			
		||||
                 .AddField(fb => fb.WithName(GetText("to_position")).WithValue($"#{to + 1}").WithIsInline(true))
 | 
			
		||||
                 .WithColor(NadekoBot.OkColor);
 | 
			
		||||
                 .WithColor(Bot.OkColor);
 | 
			
		||||
 | 
			
		||||
             if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute))
 | 
			
		||||
                 embed.WithUrl(track.Url);
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ namespace NadekoBot.Modules.Permissions.Services
 | 
			
		||||
 | 
			
		||||
        public int Priority { get; } = 0;
 | 
			
		||||
            
 | 
			
		||||
        public CmdCdService(NadekoBot bot)
 | 
			
		||||
        public CmdCdService(Bot bot)
 | 
			
		||||
        {
 | 
			
		||||
            CommandCooldowns = new ConcurrentDictionary<ulong, ConcurrentHashSet<CommandCooldown>>(
 | 
			
		||||
                bot.AllGuildConfigs.ToDictionary(k => k.GuildId, 
 | 
			
		||||
 
 | 
			
		||||
@@ -325,7 +325,7 @@ namespace NadekoBot.Modules.Searches
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(new EmbedBuilder()
 | 
			
		||||
                .WithColor(NadekoBot.OkColor)
 | 
			
		||||
                .WithColor(Bot.OkColor)
 | 
			
		||||
                .AddField(efb => efb.WithName(GetText("original_url"))
 | 
			
		||||
                                    .WithValue($"<{query}>"))
 | 
			
		||||
                .AddField(efb => efb.WithName(GetText("short_url"))
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ namespace NadekoBot.Modules.Searches.Services
 | 
			
		||||
        private readonly ConcurrentDictionary<string, DateTime> _lastPosts =
 | 
			
		||||
            new ConcurrentDictionary<string, DateTime>();
 | 
			
		||||
 | 
			
		||||
        public FeedsService(NadekoBot bot, DbService db, DiscordSocketClient client)
 | 
			
		||||
        public FeedsService(Bot bot, DbService db, DiscordSocketClient client)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,7 @@ namespace NadekoBot.Modules.Searches.Services
 | 
			
		||||
        private readonly List<string> _yomamaJokes;
 | 
			
		||||
 | 
			
		||||
        public SearchesService(DiscordSocketClient client, IGoogleApiService google,
 | 
			
		||||
            DbService db, NadekoBot bot, IDataCache cache, IHttpClientFactory factory,
 | 
			
		||||
            DbService db, Bot bot, IDataCache cache, IHttpClientFactory factory,
 | 
			
		||||
            FontProvider fonts, IBotCredentials creds)
 | 
			
		||||
        {
 | 
			
		||||
            _httpFactory = factory;
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,7 @@ namespace NadekoBot.Modules.Searches.Services
 | 
			
		||||
 | 
			
		||||
        public StreamNotificationService(DbService db, DiscordSocketClient client,
 | 
			
		||||
            IBotStrings strings, IDataCache cache, IBotCredentials creds, IHttpClientFactory httpFactory,
 | 
			
		||||
            NadekoBot bot)
 | 
			
		||||
            Bot bot)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _client = client;
 | 
			
		||||
@@ -457,7 +457,7 @@ namespace NadekoBot.Modules.Searches.Services
 | 
			
		||||
                .AddField(efb => efb.WithName(GetText(guildId, "viewers"))
 | 
			
		||||
                    .WithValue(status.IsLive ? status.Viewers.ToString() : "-")
 | 
			
		||||
                    .WithIsInline(true))
 | 
			
		||||
                .WithColor(status.IsLive ? NadekoBot.OkColor : NadekoBot.ErrorColor);
 | 
			
		||||
                .WithColor(status.IsLive ? Bot.OkColor : Bot.ErrorColor);
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(status.Title))
 | 
			
		||||
                embed.WithAuthor(status.Title);
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ namespace NadekoBot.Modules.Searches
 | 
			
		||||
                        {
 | 
			
		||||
                            var res = await http.GetStringAsync($"{_xkcdUrl}/info.0.json").ConfigureAwait(false);
 | 
			
		||||
                            var comic = JsonConvert.DeserializeObject<XkcdComic>(res);
 | 
			
		||||
                            var embed = new EmbedBuilder().WithColor(NadekoBot.OkColor)
 | 
			
		||||
                            var embed = new EmbedBuilder().WithColor(Bot.OkColor)
 | 
			
		||||
                                                      .WithImageUrl(comic.ImageLink)
 | 
			
		||||
                                                      .WithAuthor(eab => eab.WithName(comic.Title).WithUrl($"{_xkcdUrl}/{comic.Num}").WithIconUrl("https://xkcd.com/s/919f27.ico"))
 | 
			
		||||
                                                      .AddField(efb => efb.WithName(GetText("comic_number")).WithValue(comic.Num.ToString()).WithIsInline(true))
 | 
			
		||||
@@ -69,7 +69,7 @@ namespace NadekoBot.Modules.Searches
 | 
			
		||||
                        var res = await http.GetStringAsync($"{_xkcdUrl}/{num}/info.0.json").ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
                        var comic = JsonConvert.DeserializeObject<XkcdComic>(res);
 | 
			
		||||
                        var embed = new EmbedBuilder().WithColor(NadekoBot.OkColor)
 | 
			
		||||
                        var embed = new EmbedBuilder().WithColor(Bot.OkColor)
 | 
			
		||||
                                                      .WithImageUrl(comic.ImageLink)
 | 
			
		||||
                                                      .WithAuthor(eab => eab.WithName(comic.Title).WithUrl($"{_xkcdUrl}/{num}").WithIconUrl("https://xkcd.com/s/919f27.ico"))
 | 
			
		||||
                                                      .AddField(efb => efb.WithName(GetText("comic_number")).WithValue(comic.Num.ToString()).WithIsInline(true))
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,7 @@ namespace NadekoBot.Modules.Utility
 | 
			
		||||
                    .AddField(fb => fb.WithName(GetText("region")).WithValue(guild.VoiceRegionId.ToString()).WithIsInline(true))
 | 
			
		||||
                    .AddField(fb => fb.WithName(GetText("roles")).WithValue((guild.Roles.Count - 1).ToString()).WithIsInline(true))
 | 
			
		||||
                    .AddField(fb => fb.WithName(GetText("features")).WithValue(features).WithIsInline(true))
 | 
			
		||||
                    .WithColor(NadekoBot.OkColor);
 | 
			
		||||
                    .WithColor(Bot.OkColor);
 | 
			
		||||
                if (Uri.IsWellFormedUriString(guild.IconUrl, UriKind.Absolute))
 | 
			
		||||
                    embed.WithThumbnailUrl(guild.IconUrl);
 | 
			
		||||
                if (guild.Emotes.Any())
 | 
			
		||||
@@ -89,7 +89,7 @@ namespace NadekoBot.Modules.Utility
 | 
			
		||||
                    .AddField(fb => fb.WithName(GetText("id")).WithValue(ch.Id.ToString()).WithIsInline(true))
 | 
			
		||||
                    .AddField(fb => fb.WithName(GetText("created_at")).WithValue($"{createdAt:dd.MM.yyyy HH:mm}").WithIsInline(true))
 | 
			
		||||
                    .AddField(fb => fb.WithName(GetText("users")).WithValue(usercount.ToString()).WithIsInline(true))
 | 
			
		||||
                    .WithColor(NadekoBot.OkColor);
 | 
			
		||||
                    .WithColor(Bot.OkColor);
 | 
			
		||||
                await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -112,7 +112,7 @@ namespace NadekoBot.Modules.Utility
 | 
			
		||||
                    .AddField(fb => fb.WithName(GetText("joined_server")).WithValue($"{user.JoinedAt?.ToString("dd.MM.yyyy HH:mm") ?? "?"}").WithIsInline(true))
 | 
			
		||||
                    .AddField(fb => fb.WithName(GetText("joined_discord")).WithValue($"{user.CreatedAt:dd.MM.yyyy HH:mm}").WithIsInline(true))
 | 
			
		||||
                    .AddField(fb => fb.WithName(GetText("roles")).WithValue($"**({user.RoleIds.Count - 1})** - {string.Join("\n", user.GetRoles().Take(10).Where(r => r.Id != r.Guild.EveryoneRole.Id).Select(r => r.Name)).SanitizeMentions(true)}").WithIsInline(true))
 | 
			
		||||
                    .WithColor(NadekoBot.OkColor);
 | 
			
		||||
                    .WithColor(Bot.OkColor);
 | 
			
		||||
 | 
			
		||||
                var av = user.RealAvatarUrl();
 | 
			
		||||
                if (av != null && av.IsAbsoluteUri)
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ namespace NadekoBot.Modules.Utility.Services
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly ConcurrentDictionary<ulong, StreamRoleSettings> guildSettings;
 | 
			
		||||
        
 | 
			
		||||
        public StreamRoleService(DiscordSocketClient client, DbService db, NadekoBot bot)
 | 
			
		||||
        public StreamRoleService(DiscordSocketClient client, DbService db, Bot bot)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _client = client;
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ namespace NadekoBot.Modules.Utility.Services
 | 
			
		||||
        private readonly CommandHandler _ch;
 | 
			
		||||
        private readonly HelpService _hs;
 | 
			
		||||
 | 
			
		||||
        public VerboseErrorsService(NadekoBot bot, DbService db, CommandHandler ch, HelpService hs)
 | 
			
		||||
        public VerboseErrorsService(Bot bot, DbService db, CommandHandler ch, HelpService hs)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _ch = ch;
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common.Replacements;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using NadekoBot.Services;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Utility
 | 
			
		||||
@@ -23,18 +24,18 @@ namespace NadekoBot.Modules.Utility
 | 
			
		||||
    public partial class Utility : NadekoModule
 | 
			
		||||
    {
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly ICoordinator _coord;
 | 
			
		||||
        private readonly IStatsService _stats;
 | 
			
		||||
        private readonly IBotCredentials _creds;
 | 
			
		||||
        private readonly NadekoBot _bot;
 | 
			
		||||
        private readonly DownloadTracker _tracker;
 | 
			
		||||
 | 
			
		||||
        public Utility(NadekoBot nadeko, DiscordSocketClient client,
 | 
			
		||||
        public Utility(DiscordSocketClient client, ICoordinator coord,
 | 
			
		||||
            IStatsService stats, IBotCredentials creds, DownloadTracker tracker)
 | 
			
		||||
        {
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _coord = coord;
 | 
			
		||||
            _stats = stats;
 | 
			
		||||
            _creds = creds;
 | 
			
		||||
            _bot = nadeko;
 | 
			
		||||
            _tracker = tracker;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
@@ -276,7 +277,7 @@ namespace NadekoBot.Modules.Utility
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("uptime")).WithValue(_stats.GetUptimeString("\n")).WithIsInline(true))
 | 
			
		||||
                    .AddField(efb => efb.WithName(GetText("presence")).WithValue(
 | 
			
		||||
                        GetText("presence_txt",
 | 
			
		||||
                            _bot.GuildCount, _stats.TextChannels, _stats.VoiceChannels)).WithIsInline(true))).ConfigureAwait(false);
 | 
			
		||||
                            _coord.GetGuildCount(), _stats.TextChannels, _stats.VoiceChannels)).WithIsInline(true))).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [NadekoCommand, Usage, Description, Aliases]
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,7 @@ namespace NadekoBot.Modules.Xp.Services
 | 
			
		||||
        private XpTemplate _template;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
 | 
			
		||||
        public XpService(DiscordSocketClient client, CommandHandler cmd, NadekoBot bot, DbService db,
 | 
			
		||||
        public XpService(DiscordSocketClient client, CommandHandler cmd, Bot bot, DbService db,
 | 
			
		||||
            IBotStrings strings, IDataCache cache, FontProvider fonts, IBotCredentials creds,
 | 
			
		||||
            ICurrencyService cs, IHttpClientFactory http, XpConfigService xpConfig)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,12 @@
 | 
			
		||||
    <PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138" />
 | 
			
		||||
    <PackageReference Include="Google.Apis.YouTube.v3" Version="1.52.0.2343" />
 | 
			
		||||
    <PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084" />
 | 
			
		||||
    <PackageReference Include="Google.Protobuf" Version="3.13.0" />
 | 
			
		||||
    <PackageReference Include="Grpc.Net.ClientFactory" Version="2.32.0" />
 | 
			
		||||
    <PackageReference Include="Grpc.Tools" Version="2.32.0">
 | 
			
		||||
      <PrivateAssets>all</PrivateAssets>
 | 
			
		||||
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
			
		||||
    </PackageReference>
 | 
			
		||||
    <PackageReference Include="Html2Markdown" Version="4.0.0.427" />
 | 
			
		||||
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.7" />
 | 
			
		||||
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.7">
 | 
			
		||||
@@ -52,6 +58,9 @@
 | 
			
		||||
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <Compile Remove="credentials.json" />
 | 
			
		||||
    <Protobuf Include="..\NadekoBot.Coordinator\Protos\coordinator.proto" GrpcServices="Client">
 | 
			
		||||
      <Link>Protos\coordinator.proto</Link>
 | 
			
		||||
    </Protobuf>
 | 
			
		||||
    <None Update="data\**\*">
 | 
			
		||||
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
 | 
			
		||||
    </None>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,34 +1,14 @@
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot
 | 
			
		||||
{
 | 
			
		||||
    public sealed class Program
 | 
			
		||||
    {
 | 
			
		||||
        public static async Task Main(string[] args)
 | 
			
		||||
        {
 | 
			
		||||
            var pid = Process.GetCurrentProcess().Id;
 | 
			
		||||
            System.Console.WriteLine($"Pid: {pid}");
 | 
			
		||||
            if (args.Length == 2
 | 
			
		||||
                && int.TryParse(args[0], out int shardId)
 | 
			
		||||
                && int.TryParse(args[1], out int parentProcessId))
 | 
			
		||||
            {
 | 
			
		||||
                await new NadekoBot(shardId, parentProcessId == 0 ? pid : parentProcessId)
 | 
			
		||||
                    .RunAndBlockAsync();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await new ShardsCoordinator()
 | 
			
		||||
                    .RunAsync()
 | 
			
		||||
                    .ConfigureAwait(false);
 | 
			
		||||
#if DEBUG
 | 
			
		||||
                await new NadekoBot(0, pid)
 | 
			
		||||
                    .RunAndBlockAsync();
 | 
			
		||||
#else
 | 
			
		||||
                await Task.Delay(-1);
 | 
			
		||||
#endif
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
var pid = System.Environment.ProcessId;
 | 
			
		||||
 | 
			
		||||
var shardId = 0;
 | 
			
		||||
if (args.Length == 1)
 | 
			
		||||
    int.TryParse(args[0], out shardId);
 | 
			
		||||
 | 
			
		||||
LogSetup.SetupLogger(shardId);
 | 
			
		||||
Log.Information($"Pid: {pid}");
 | 
			
		||||
 | 
			
		||||
await new Bot(shardId).RunAndBlockAsync();
 | 
			
		||||
@@ -37,7 +37,7 @@ namespace NadekoBot.Core.Services
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
        private readonly CommandService _commandService;
 | 
			
		||||
        private readonly BotConfigService _bss;
 | 
			
		||||
        private readonly NadekoBot _bot;
 | 
			
		||||
        private readonly Bot _bot;
 | 
			
		||||
        private IServiceProvider _services;
 | 
			
		||||
        private IEnumerable<IEarlyBehavior> _earlyBehaviors;
 | 
			
		||||
        private IEnumerable<IInputTransformer> _inputTransformers;
 | 
			
		||||
@@ -57,7 +57,7 @@ namespace NadekoBot.Core.Services
 | 
			
		||||
        private readonly Timer _clearUsersOnShortCooldown;
 | 
			
		||||
 | 
			
		||||
        public CommandHandler(DiscordSocketClient client, DbService db, CommandService commandService,
 | 
			
		||||
            BotConfigService bss, NadekoBot bot, IServiceProvider services)
 | 
			
		||||
            BotConfigService bss, Bot bot, IServiceProvider services)
 | 
			
		||||
        {
 | 
			
		||||
            _client = client;
 | 
			
		||||
            _commandService = commandService;
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ namespace NadekoBot.Core.Services
 | 
			
		||||
        private readonly BotConfigService _bss;
 | 
			
		||||
        public bool GroupGreets => _bss.Data.GroupGreets;
 | 
			
		||||
 | 
			
		||||
        public GreetSettingsService(DiscordSocketClient client, NadekoBot bot, DbService db,
 | 
			
		||||
        public GreetSettingsService(DiscordSocketClient client, Bot bot, DbService db,
 | 
			
		||||
            BotConfigService bss)
 | 
			
		||||
        {
 | 
			
		||||
            _db = db;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								src/NadekoBot/Services/ICoordinator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/NadekoBot/Services/ICoordinator.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Services
 | 
			
		||||
{
 | 
			
		||||
    public interface ICoordinator
 | 
			
		||||
    {
 | 
			
		||||
        bool RestartBot();
 | 
			
		||||
        void Die();
 | 
			
		||||
        bool RestartShard(int shardId);
 | 
			
		||||
        IEnumerable<ShardStatus> GetAllShardStatuses();
 | 
			
		||||
        int GetGuildCount();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public class ShardStatus
 | 
			
		||||
    {
 | 
			
		||||
        public Discord.ConnectionState ConnectionState { get; set; }
 | 
			
		||||
        public DateTime Time { get; set; }
 | 
			
		||||
        public int ShardId { get; set; }
 | 
			
		||||
        public int Guilds { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -22,7 +22,7 @@ namespace NadekoBot.Core.Services.Impl
 | 
			
		||||
        private static readonly Dictionary<string, CommandData> _commandData = JsonConvert.DeserializeObject<Dictionary<string, CommandData>>(
 | 
			
		||||
                File.ReadAllText("./data/strings/commands/commands.en-US.json"));
 | 
			
		||||
 | 
			
		||||
        public Localization(BotConfigService bss, NadekoBot bot, DbService db)
 | 
			
		||||
        public Localization(BotConfigService bss, Bot bot, DbService db)
 | 
			
		||||
        {
 | 
			
		||||
            _bss = bss;
 | 
			
		||||
            _db = db;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										138
									
								
								src/NadekoBot/Services/Impl/RemoteGrpcCoordinator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								src/NadekoBot/Services/Impl/RemoteGrpcCoordinator.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Discord;
 | 
			
		||||
using Discord.WebSocket;
 | 
			
		||||
using Grpc.Core;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Coordinator;
 | 
			
		||||
using NadekoBot.Core.Services;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Services
 | 
			
		||||
{
 | 
			
		||||
    public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor
 | 
			
		||||
    {
 | 
			
		||||
        private readonly Coordinator.Coordinator.CoordinatorClient _coordClient;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
 | 
			
		||||
        public RemoteGrpcCoordinator(IBotCredentials creds, DiscordSocketClient client)
 | 
			
		||||
        {
 | 
			
		||||
            // todo should use credentials
 | 
			
		||||
            var channel = Grpc.Net.Client.GrpcChannel.ForAddress("https://localhost:3443");
 | 
			
		||||
            _coordClient = new(channel);
 | 
			
		||||
            _client = client;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        public bool RestartBot()
 | 
			
		||||
        {
 | 
			
		||||
            _coordClient.RestartAllShards(new RestartAllRequest
 | 
			
		||||
            { 
 | 
			
		||||
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void Die()
 | 
			
		||||
        {
 | 
			
		||||
            _coordClient.Die(new DieRequest()
 | 
			
		||||
            {
 | 
			
		||||
                Graceful = false
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public bool RestartShard(int shardId)
 | 
			
		||||
        {
 | 
			
		||||
            _coordClient.RestartShard(new RestartShardRequest
 | 
			
		||||
            {
 | 
			
		||||
                ShardId = shardId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public IEnumerable<ShardStatus> GetAllShardStatuses()
 | 
			
		||||
        {
 | 
			
		||||
            var res = _coordClient.GetAllStatuses(new GetAllStatusesRequest());
 | 
			
		||||
 | 
			
		||||
            return res.Statuses
 | 
			
		||||
                .ToArray()
 | 
			
		||||
                .Map(s => new ShardStatus()
 | 
			
		||||
                {
 | 
			
		||||
                    ConnectionState = FromCoordConnState(s.State),
 | 
			
		||||
                    Guilds = s.GuildCount,
 | 
			
		||||
                    ShardId = s.ShardId,
 | 
			
		||||
                    Time = s.LastUpdate.ToDateTime(),
 | 
			
		||||
                });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public int GetGuildCount()
 | 
			
		||||
        {
 | 
			
		||||
            var res = _coordClient.GetAllStatuses(new GetAllStatusesRequest());
 | 
			
		||||
 | 
			
		||||
            return res.Statuses.Sum(x => x.GuildCount);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Task OnReadyAsync()
 | 
			
		||||
        {
 | 
			
		||||
            Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                var gracefulImminent = false;
 | 
			
		||||
                while (true)
 | 
			
		||||
                {
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        var reply = await _coordClient.HeartbeatAsync(new HeartbeatRequest
 | 
			
		||||
                        {
 | 
			
		||||
                            State = ToCoordConnState(_client.ConnectionState),
 | 
			
		||||
                            GuildCount = _client.ConnectionState == Discord.ConnectionState.Connected ? _client.Guilds.Count : 0,
 | 
			
		||||
                            ShardId = _client.ShardId,
 | 
			
		||||
                        }, deadline: DateTime.UtcNow + TimeSpan.FromSeconds(10));
 | 
			
		||||
                        gracefulImminent = reply.GracefulImminent;
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (RpcException ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (!gracefulImminent)
 | 
			
		||||
                        {
 | 
			
		||||
                            Log.Warning(ex, "Hearbeat failed and graceful shutdown was not expected: {Message}",
 | 
			
		||||
                                ex.Message);
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        await Task.Delay(22500).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex)
 | 
			
		||||
                    {
 | 
			
		||||
                        Log.Error(ex, "Unexpected heartbeat exception: {Message}", ex.Message);
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await Task.Delay(7500).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Environment.Exit(5);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private ConnState ToCoordConnState(Discord.ConnectionState state)
 | 
			
		||||
            => state switch
 | 
			
		||||
            {
 | 
			
		||||
                Discord.ConnectionState.Connecting => ConnState.Connecting,
 | 
			
		||||
                Discord.ConnectionState.Connected => ConnState.Connected,
 | 
			
		||||
                _ => ConnState.Disconnected
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
        private Discord.ConnectionState FromCoordConnState(ConnState state)
 | 
			
		||||
            => state switch
 | 
			
		||||
            {
 | 
			
		||||
                ConnState.Connecting => Discord.ConnectionState.Connecting,
 | 
			
		||||
                ConnState.Connected => Discord.ConnectionState.Connected,
 | 
			
		||||
                _ => Discord.ConnectionState.Disconnected
 | 
			
		||||
            };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -37,9 +37,9 @@ namespace NadekoBot.Core.Services
 | 
			
		||||
            var error = _data.Color.Error;
 | 
			
		||||
            var pend = _data.Color.Pending;
 | 
			
		||||
            // todo future remove these static props once cleanup is done
 | 
			
		||||
            NadekoBot.OkColor = new Color(ok.R, ok.G, ok.B);
 | 
			
		||||
            NadekoBot.ErrorColor = new Color(error.R, error.G, error.B);
 | 
			
		||||
            NadekoBot.PendingColor = new Color(pend.R, pend.G, pend.B);
 | 
			
		||||
            Bot.OkColor = new Color(ok.R, ok.G, ok.B);
 | 
			
		||||
            Bot.ErrorColor = new Color(error.R, error.G, error.B);
 | 
			
		||||
            Bot.PendingColor = new Color(pend.R, pend.G, pend.B);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        protected override void OnStateUpdate()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,388 +0,0 @@
 | 
			
		||||
using NadekoBot.Common.Collections;
 | 
			
		||||
using NadekoBot.Common.ShardCom;
 | 
			
		||||
using NadekoBot.Core.Services.Impl;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using StackExchange.Redis;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Core.Common;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Core.Services
 | 
			
		||||
{
 | 
			
		||||
    public class ShardsCoordinator
 | 
			
		||||
    {
 | 
			
		||||
        private class ShardsCoordinatorQueue
 | 
			
		||||
        {
 | 
			
		||||
            private readonly object _locker = new object();
 | 
			
		||||
            private readonly HashSet<int> _set = new HashSet<int>();
 | 
			
		||||
            private readonly Queue<int> _queue = new Queue<int>();
 | 
			
		||||
            public int Count => _queue.Count;
 | 
			
		||||
 | 
			
		||||
            public void Enqueue(int i)
 | 
			
		||||
            {
 | 
			
		||||
                lock (_locker)
 | 
			
		||||
                {
 | 
			
		||||
                    if (_set.Add(i))
 | 
			
		||||
                        _queue.Enqueue(i);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public bool TryPeek(out int id)
 | 
			
		||||
            {
 | 
			
		||||
                lock (_locker)
 | 
			
		||||
                {
 | 
			
		||||
                    return _queue.TryPeek(out id);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            public bool TryDequeue(out int id)
 | 
			
		||||
            {
 | 
			
		||||
                lock (_locker)
 | 
			
		||||
                {
 | 
			
		||||
                    if (_queue.TryDequeue(out id))
 | 
			
		||||
                    {
 | 
			
		||||
                        _set.Remove(id);
 | 
			
		||||
                        return true;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private readonly BotCredentials _creds;
 | 
			
		||||
        private readonly string _key;
 | 
			
		||||
        private readonly Process[] _shardProcesses;
 | 
			
		||||
 | 
			
		||||
        private readonly int _curProcessId;
 | 
			
		||||
        private readonly ConnectionMultiplexer _redis;
 | 
			
		||||
        private ShardComMessage _defaultShardState;
 | 
			
		||||
 | 
			
		||||
        private ShardsCoordinatorQueue _shardStartQueue =
 | 
			
		||||
            new ShardsCoordinatorQueue();
 | 
			
		||||
 | 
			
		||||
        private ConcurrentHashSet<int> _shardRestartWaitingList =
 | 
			
		||||
            new ConcurrentHashSet<int>();
 | 
			
		||||
 | 
			
		||||
        public ShardsCoordinator()
 | 
			
		||||
        {
 | 
			
		||||
            //load main stuff
 | 
			
		||||
            LogSetup.SetupLogger("coord");
 | 
			
		||||
            _creds = new BotCredentials();
 | 
			
		||||
 | 
			
		||||
            Log.Information("Starting NadekoBot v" + StatsService.BotVersion);
 | 
			
		||||
 | 
			
		||||
            _key = _creds.RedisKey();
 | 
			
		||||
 | 
			
		||||
            var conf = ConfigurationOptions.Parse(_creds.RedisOptions);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                _redis = ConnectionMultiplexer.Connect(conf);
 | 
			
		||||
            }
 | 
			
		||||
            catch (RedisConnectionException ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error(ex, "Redis error. Make sure Redis is installed and running as a service");
 | 
			
		||||
                Helpers.ReadErrorAndExit(11);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var imgCache = new RedisImagesCache(_redis, _creds); //reload images into redis
 | 
			
		||||
            if (!imgCache.AllKeysExist().GetAwaiter().GetResult()) // but only if the keys don't exist. If images exist, you have to reload them manually
 | 
			
		||||
            {
 | 
			
		||||
                imgCache.Reload().GetAwaiter().GetResult();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                Log.Information("Images are already present in redis. Use .imagesreload to force update if needed");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            //setup initial shard statuses
 | 
			
		||||
            _defaultShardState = new ShardComMessage()
 | 
			
		||||
            {
 | 
			
		||||
                ConnectionState = Discord.ConnectionState.Disconnected,
 | 
			
		||||
                Guilds = 0,
 | 
			
		||||
                Time = DateTime.UtcNow
 | 
			
		||||
            };
 | 
			
		||||
            var db = _redis.GetDatabase();
 | 
			
		||||
            //clear previous statuses
 | 
			
		||||
            db.KeyDelete(_key + "_shardstats");
 | 
			
		||||
 | 
			
		||||
            _shardProcesses = new Process[_creds.TotalShards];
 | 
			
		||||
 | 
			
		||||
#if GLOBAL_NADEKO
 | 
			
		||||
            var shardIdsEnum = Enumerable.Range(1, 31)
 | 
			
		||||
                .Concat(Enumerable.Range(33, _creds.TotalShards - 33))
 | 
			
		||||
                .Shuffle()
 | 
			
		||||
                .Prepend(32)
 | 
			
		||||
                .Prepend(0);
 | 
			
		||||
#else
 | 
			
		||||
            var shardIdsEnum = Enumerable.Range(1, _creds.TotalShards - 1)
 | 
			
		||||
                .Shuffle()
 | 
			
		||||
                .Prepend(0);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
            var shardIds = shardIdsEnum
 | 
			
		||||
                .ToArray();
 | 
			
		||||
            for (var i = 0; i < shardIds.Length; i++)
 | 
			
		||||
            {
 | 
			
		||||
                var id = shardIds[i];
 | 
			
		||||
                //add it to the list of shards which should be started
 | 
			
		||||
#if DEBUG
 | 
			
		||||
                if (id > 0)
 | 
			
		||||
                    _shardStartQueue.Enqueue(id);
 | 
			
		||||
                else
 | 
			
		||||
                    _shardProcesses[id] = Process.GetCurrentProcess();
 | 
			
		||||
#else
 | 
			
		||||
                _shardStartQueue.Enqueue(id);
 | 
			
		||||
#endif
 | 
			
		||||
                //set the shard's initial state in redis cache
 | 
			
		||||
                var msg = _defaultShardState.Clone();
 | 
			
		||||
                msg.ShardId = id;
 | 
			
		||||
                //this is to avoid the shard coordinator thinking that
 | 
			
		||||
                //the shard is unresponsive while starting up
 | 
			
		||||
                var delay = 45;
 | 
			
		||||
#if GLOBAL_NADEKO
 | 
			
		||||
                delay = 180;
 | 
			
		||||
#endif
 | 
			
		||||
                msg.Time = DateTime.UtcNow + TimeSpan.FromSeconds(delay * (id + 1));
 | 
			
		||||
                db.ListRightPush(_key + "_shardstats",
 | 
			
		||||
                    JsonConvert.SerializeObject(msg),
 | 
			
		||||
                    flags: CommandFlags.FireAndForget);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _curProcessId = Process.GetCurrentProcess().Id;
 | 
			
		||||
 | 
			
		||||
            //subscribe to shardcoord events
 | 
			
		||||
            var sub = _redis.GetSubscriber();
 | 
			
		||||
 | 
			
		||||
            //send is called when shard status is updated. Every 7.5 seconds atm
 | 
			
		||||
            sub.Subscribe(_key + "_shardcoord_send",
 | 
			
		||||
                OnDataReceived,
 | 
			
		||||
                CommandFlags.FireAndForget);
 | 
			
		||||
 | 
			
		||||
            //called to stop the shard, although the shard will start again when it finds out it's dead
 | 
			
		||||
            sub.Subscribe(_key + "_shardcoord_stop",
 | 
			
		||||
                OnStop,
 | 
			
		||||
                CommandFlags.FireAndForget);
 | 
			
		||||
 | 
			
		||||
            //called kill the bot
 | 
			
		||||
            sub.Subscribe(_key + "_die",
 | 
			
		||||
                (ch, x) => Environment.Exit(0),
 | 
			
		||||
                CommandFlags.FireAndForget);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void OnStop(RedisChannel ch, RedisValue data)
 | 
			
		||||
        {
 | 
			
		||||
            var shardId = JsonConvert.DeserializeObject<int>(data);
 | 
			
		||||
            OnStop(shardId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void OnStop(int shardId)
 | 
			
		||||
        {
 | 
			
		||||
            var db = _redis.GetDatabase();
 | 
			
		||||
            var msg = _defaultShardState.Clone();
 | 
			
		||||
            msg.ShardId = shardId;
 | 
			
		||||
            db.ListSetByIndex(_key + "_shardstats",
 | 
			
		||||
                    shardId,
 | 
			
		||||
                    JsonConvert.SerializeObject(msg),
 | 
			
		||||
                    CommandFlags.FireAndForget);
 | 
			
		||||
            var p = _shardProcesses[shardId];
 | 
			
		||||
            if (p is null)
 | 
			
		||||
                return; // ignore
 | 
			
		||||
            _shardProcesses[shardId] = null;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                p.KillTree();
 | 
			
		||||
                p.Dispose();
 | 
			
		||||
            }
 | 
			
		||||
            catch { }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void OnDataReceived(RedisChannel ch, RedisValue data)
 | 
			
		||||
        {
 | 
			
		||||
            var msg = JsonConvert.DeserializeObject<ShardComMessage>(data);
 | 
			
		||||
            if (msg is null)
 | 
			
		||||
                return;
 | 
			
		||||
            var db = _redis.GetDatabase();
 | 
			
		||||
            //sets the shard state
 | 
			
		||||
            db.ListSetByIndex(_key + "_shardstats",
 | 
			
		||||
                    msg.ShardId,
 | 
			
		||||
                    data,
 | 
			
		||||
                    CommandFlags.FireAndForget);
 | 
			
		||||
            if (msg.ConnectionState == Discord.ConnectionState.Disconnected
 | 
			
		||||
                || msg.ConnectionState == Discord.ConnectionState.Disconnecting)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error("!!! SHARD {0} IS IN {1} STATE !!!", msg.ShardId, msg.ConnectionState.ToString());
 | 
			
		||||
 | 
			
		||||
                OnShardUnavailable(msg.ShardId);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                // remove the shard from the waiting list if it's on it,
 | 
			
		||||
                // because it's connected/connecting now
 | 
			
		||||
                _shardRestartWaitingList.TryRemove(msg.ShardId);
 | 
			
		||||
            }
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void OnShardUnavailable(int shardId)
 | 
			
		||||
        {
 | 
			
		||||
            //if the shard is dc'd, add it to the restart waiting list
 | 
			
		||||
            if (!_shardRestartWaitingList.Add(shardId))
 | 
			
		||||
            {
 | 
			
		||||
                //if it's already on the waiting list
 | 
			
		||||
                //stop the shard
 | 
			
		||||
                OnStop(shardId);
 | 
			
		||||
                //add it to the start queue (start the shard)
 | 
			
		||||
                _shardStartQueue.Enqueue(shardId);
 | 
			
		||||
                //remove it from the waiting list
 | 
			
		||||
                _shardRestartWaitingList.TryRemove(shardId);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task RunAsync()
 | 
			
		||||
        {
 | 
			
		||||
            //this task will complete when the initial start of the shards 
 | 
			
		||||
            //is complete, but will keep running in order to restart shards
 | 
			
		||||
            //which are disconnected for too long
 | 
			
		||||
            TaskCompletionSource<bool> tsc = new TaskCompletionSource<bool>();
 | 
			
		||||
            var _ = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                do
 | 
			
		||||
                {
 | 
			
		||||
                    //start a shard which is scheduled for start every 6 seconds 
 | 
			
		||||
                    while (_shardStartQueue.TryPeek(out var id))
 | 
			
		||||
                    {
 | 
			
		||||
                        // if the shard is on the waiting list again
 | 
			
		||||
                        // remove it since it's starting up now
 | 
			
		||||
 | 
			
		||||
                        _shardRestartWaitingList.TryRemove(id);
 | 
			
		||||
                        //if the task is already completed,
 | 
			
		||||
                        //it means the initial shard starting is done,
 | 
			
		||||
                        //and this is an auto-restart
 | 
			
		||||
                        if (tsc.Task.IsCompleted)
 | 
			
		||||
                        {
 | 
			
		||||
                            Log.Warning("Auto-restarting shard {0}, {1} more in queue.", id, _shardStartQueue.Count);
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            Log.Warning("Starting shard {0}, {1} more in queue.", id, _shardStartQueue.Count - 1);
 | 
			
		||||
                        }
 | 
			
		||||
                        var rem = _shardProcesses[id];
 | 
			
		||||
                        if (rem != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            try
 | 
			
		||||
                            {
 | 
			
		||||
                                rem.KillTree();
 | 
			
		||||
                                rem.Dispose();
 | 
			
		||||
                            }
 | 
			
		||||
                            catch { }
 | 
			
		||||
                        }
 | 
			
		||||
                        _shardProcesses[id] = StartShard(id);
 | 
			
		||||
                        _shardStartQueue.TryDequeue(out var __);
 | 
			
		||||
                        await Task.Delay(10000).ConfigureAwait(false);
 | 
			
		||||
                    }
 | 
			
		||||
                    tsc.TrySetResult(true);
 | 
			
		||||
                    await Task.Delay(6000).ConfigureAwait(false);
 | 
			
		||||
                }
 | 
			
		||||
                while (true);
 | 
			
		||||
                // ^ keep checking for shards which need to be restarted
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            //restart unresponsive shards
 | 
			
		||||
            _ = Task.Run(async () =>
 | 
			
		||||
            {
 | 
			
		||||
                //after all shards have started initially
 | 
			
		||||
                await tsc.Task.ConfigureAwait(false);
 | 
			
		||||
                while (true)
 | 
			
		||||
                {
 | 
			
		||||
                    await Task.Delay(15000).ConfigureAwait(false);
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        var db = _redis.GetDatabase();
 | 
			
		||||
                        //get all shards which didn't communicate their status in the last 30 seconds
 | 
			
		||||
                        var all = db.ListRange(_creds.RedisKey() + "_shardstats")
 | 
			
		||||
                           .Select(x => JsonConvert.DeserializeObject<ShardComMessage>(x));
 | 
			
		||||
                        var statuses = all
 | 
			
		||||
                           .Where(x => x.Time < DateTime.UtcNow - TimeSpan.FromSeconds(30))
 | 
			
		||||
                           .ToArray();
 | 
			
		||||
 | 
			
		||||
                        if (!statuses.Any())
 | 
			
		||||
                        {
 | 
			
		||||
#if DEBUG
 | 
			
		||||
                            for (var i = 0; i < _shardProcesses.Length; i++)
 | 
			
		||||
                            {
 | 
			
		||||
                                var p = _shardProcesses[i];
 | 
			
		||||
                                if (p is null || p.HasExited)
 | 
			
		||||
                                {
 | 
			
		||||
                                    Log.Warning("Scheduling shard {0} for restart because it's process is stopped.", i);
 | 
			
		||||
                                    _shardStartQueue.Enqueue(i);
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
#endif
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            for (var i = 0; i < statuses.Length; i++)
 | 
			
		||||
                            {
 | 
			
		||||
                                var s = statuses[i];
 | 
			
		||||
                                OnStop(s.ShardId);
 | 
			
		||||
                                _shardStartQueue.Enqueue(s.ShardId);
 | 
			
		||||
 | 
			
		||||
                                //to prevent shards which are already scheduled for restart to be scheduled again
 | 
			
		||||
                                s.Time = DateTime.UtcNow + TimeSpan.FromSeconds(60 * _shardStartQueue.Count);
 | 
			
		||||
                                db.ListSetByIndex(_key + "_shardstats", s.ShardId,
 | 
			
		||||
                                    JsonConvert.SerializeObject(s), CommandFlags.FireAndForget);
 | 
			
		||||
                                Log.Warning("Shard {0} is scheduled for a restart because it's unresponsive.", s.ShardId);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    catch (Exception ex) { Log.Error(ex, "Error in RunAsync"); throw; }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            await tsc.Task.ConfigureAwait(false);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Process StartShard(int shardId)
 | 
			
		||||
        {
 | 
			
		||||
            return Process.Start(new ProcessStartInfo()
 | 
			
		||||
            {
 | 
			
		||||
                FileName = _creds.ShardRunCommand,
 | 
			
		||||
                Arguments = string.Format(_creds.ShardRunArguments, shardId, _curProcessId, "")
 | 
			
		||||
            });
 | 
			
		||||
            // last "" in format is for backwards compatibility
 | 
			
		||||
            // because current startup commands have {2} in them probably
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task RunAndBlockAsync()
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                await RunAsync().ConfigureAwait(false);
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error(ex, "Unhandled exception in RunAsync");
 | 
			
		||||
                foreach (var p in _shardProcesses)
 | 
			
		||||
                {
 | 
			
		||||
                    if (p is null)
 | 
			
		||||
                        continue;
 | 
			
		||||
                    try
 | 
			
		||||
                    {
 | 
			
		||||
                        p.KillTree();
 | 
			
		||||
                        p.Dispose();
 | 
			
		||||
                    }
 | 
			
		||||
                    catch { }
 | 
			
		||||
                }
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await Task.Delay(-1).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -184,13 +184,13 @@ namespace NadekoBot.Extensions
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static EmbedBuilder WithOkColor(this EmbedBuilder eb) =>
 | 
			
		||||
            eb.WithColor(NadekoBot.OkColor);
 | 
			
		||||
            eb.WithColor(Bot.OkColor);
 | 
			
		||||
 | 
			
		||||
        public static EmbedBuilder WithPendingColor(this EmbedBuilder eb) =>
 | 
			
		||||
            eb.WithColor(NadekoBot.PendingColor);
 | 
			
		||||
            eb.WithColor(Bot.PendingColor);
 | 
			
		||||
        
 | 
			
		||||
        public static EmbedBuilder WithErrorColor(this EmbedBuilder eb) =>
 | 
			
		||||
            eb.WithColor(NadekoBot.ErrorColor);
 | 
			
		||||
            eb.WithColor(Bot.ErrorColor);
 | 
			
		||||
 | 
			
		||||
        public static ReactionEventWrapper OnReaction(this IUserMessage msg, DiscordSocketClient client, Func<SocketReaction, Task> reactionAdded, Func<SocketReaction, Task> reactionRemoved = null)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user