using Discord; using Discord.Commands; using Discord.Net; using Discord.WebSocket; using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; using Managing.Application.Trading; using Managing.Application.Trading.Commands; using Managing.Application.Workers.Abstractions; using Managing.Common; using Managing.Core; using Managing.Domain.Statistics; using Managing.Domain.Trades; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using static Managing.Common.Enums; namespace Managing.Infrastructure.Messengers.Discord { public class DiscordService : IHostedService, IDisposable, IDiscordService { private const string _separator = "|"; private readonly DiscordSocketClient _client; private readonly CommandService _commandService; private readonly IServiceProvider _services; private readonly IExchangeService _exchangeService; private readonly IMoneyManagementService _moneyManagementService; private readonly IAccountService _accountService; private readonly ITradingService _tradingService; private readonly IStatisticService _statisticService; private readonly DiscordSettings _settings; private readonly ILogger _logger; private readonly string _explorerUrl = ""; public DiscordService(DiscordSocketClient client, CommandService commandService, IServiceProvider services, DiscordSettings settings, ILogger logger) { _client = client; _commandService = commandService; _services = services; _settings = settings; _logger = logger; } #region Setup // The hosted service has started public async Task StartAsync(CancellationToken cancellationToken) { if (_settings.HandleUserAction) { _client.ButtonExecuted += ButtonHandler; _commandService.CommandExecuted += CommandExecuted; _client.SlashCommandExecuted += SlashCommandHandler; } _client.Ready += ClientReady; _client.Log += Log; _commandService.Log += Log; // look for classes implementing ModuleBase to load commands from await _commandService.AddModulesAsync(GetType().Assembly, _services); // log in to Discord, using the provided token await _client.LoginAsync(TokenType.Bot, _settings.Token); // start bot await _client.StartAsync(); } private async Task SlashCommandHandler(SocketSlashCommand command) { await command.DeferAsync(); // Let's add a switch statement for the command name so we can handle multiple commands in one event. switch (command.Data.Name) { case Constants.DiscordSlashCommand.Leaderboard: await SlashCommands.HandleLeaderboardCommand(_services, command); break; case Constants.DiscordSlashCommand.Noobiesboard: await SlashCommands.HandleNoobiesboardCommand(_services, command); break; case Constants.DiscordSlashCommand.LeaderboardPosition: await SlashCommands.HandleLeadboardPositionCommand(_services, command); break; case Constants.DiscordSlashCommand.FundingRates: await SlashCommands.HandleFundingRateCommand(_services, command); break; } } private async Task ClientReady() { // set status to online await _client.SetStatusAsync(UserStatus.Online); // Discord started as a game chat service, so it has the option to show what games you are playing // Here the bot will display "Playing dead" while listening await _client.SetGameAsync(_settings.BotActivity, "https://moon.com", ActivityType.Playing); if (!_settings.HandleUserAction) return; List applicationCommandProperties = new(); try { var leaderBoardCommand = new SlashCommandBuilder(); leaderBoardCommand.WithName(Constants.DiscordSlashCommand.Leaderboard); leaderBoardCommand.WithDescription("Shows the last leaderboard"); applicationCommandProperties.Add(leaderBoardCommand.Build()); var leadboardPositionsCommand = new SlashCommandBuilder(); leadboardPositionsCommand.WithName(Constants.DiscordSlashCommand.LeaderboardPosition); leadboardPositionsCommand.WithDescription("Shows the opened position of the leaderboard"); applicationCommandProperties.Add(leadboardPositionsCommand.Build()); var noobiesboardCommand = new SlashCommandBuilder(); noobiesboardCommand.WithName(Constants.DiscordSlashCommand.Noobiesboard); noobiesboardCommand.WithDescription("Shows the last Noobies board"); applicationCommandProperties.Add(noobiesboardCommand.Build()); var fundingRatesCommand = new SlashCommandBuilder(); fundingRatesCommand.WithName(Constants.DiscordSlashCommand.FundingRates); fundingRatesCommand.WithDescription("Shows the last funding rates"); applicationCommandProperties.Add(fundingRatesCommand.Build()); await _client.BulkOverwriteGlobalApplicationCommandsAsync(applicationCommandProperties.ToArray()); } catch (ApplicationCommandException exception) { var json = JsonConvert.SerializeObject(exception, Formatting.Indented); _logger.LogError(exception, json); } } public static List GetSlashCommands() { List commands = new(); return commands; } // logging private async Task Log(LogMessage arg) { await Task.Run(() => { _logger.LogInformation(arg.ToString()); }); } private async Task CommandExecuted(Optional command, ICommandContext context, IResult result) { // if a command isn't found if (!command.IsSpecified) { await context.Message.AddReactionAsync(new Emoji("🤨")); // eyebrow raised emoji return; } // log failure to the console if (!result.IsSuccess) { await Log(new LogMessage(LogSeverity.Error, nameof(CommandExecuted), $"Error: {result.ErrorReason}")); return; } // react to message await context.Message.AddReactionAsync(new Emoji("🤖")); // robot emoji } // the hosted service is stopping public async Task StopAsync(CancellationToken cancellationToken) { await _client.SetGameAsync(null); await _client.SetStatusAsync(UserStatus.Offline); await _client.StopAsync(); _client.Log -= Log; _client.Ready -= ClientReady; _commandService.Log -= Log; _commandService.CommandExecuted -= CommandExecuted; _client.ButtonExecuted -= ButtonHandler; _client.SlashCommandExecuted -= SlashCommandHandler; } public void Dispose() { _client?.Dispose(); } #endregion #region In public async Task ButtonHandler(SocketMessageComponent component) { var parameters = component.Data.CustomId.Split(new[] { _separator }, StringSplitOptions.None); var command = parameters[0]; if (component.User.GlobalName != "crypto_saitama") { await component.Channel.SendMessageAsync( "Sorry bro, this feature is not accessible for you.. Do not hesitate to send me approx. 456 121 $ and i give you full access"); } else { switch (command) { case Constants.DiscordButtonAction.OpenPosition: await OpenPosition(component, parameters); break; case Constants.DiscordButtonAction.ClosePosition: await ClosePosition(component, parameters); break; case Constants.DiscordButtonAction.CopyPosition: await CopyPosition(component, parameters); break; default: break; } } } private async Task CopyPosition(SocketMessageComponent component, string[] parameters) { await component.Channel.SendMessageAsync("Let met few seconds to copy this position"); await component.Channel.TriggerTypingAsync(); var json = MiscExtensions.Base64Decode(parameters[1]); var trade = JsonConvert.DeserializeObject(json); await OpenPosition(component, trade.AccountName, trade.MoneyManagementName, PositionInitiator.CopyTrading, trade.Ticker, trade.Direction, Timeframe.FifteenMinutes, DateTime.Now.AddMinutes(trade.ExpirationMinute), true, trade.Leverage); ; } public async Task OpenPosition(SocketMessageComponent component, string[] parameters) { await component.Channel.SendMessageAsync("Let met few seconds to open a position"); await component.Channel.TriggerTypingAsync(); var accountName = MiscExtensions.ParseEnum(parameters[1]); var ticker = MiscExtensions.ParseEnum(parameters[2]); var direction = MiscExtensions.ParseEnum(parameters[3]); var timeframe = MiscExtensions.ParseEnum(parameters[4]); var moneyManagementName = parameters[5]; var expiration = DateTime.Parse(parameters[6]); await OpenPosition(component, accountName, moneyManagementName, PositionInitiator.User, ticker, direction, timeframe, expiration, false); } private async Task OpenPosition(SocketMessageComponent component, string accountName, string moneyManagement, PositionInitiator initiator, Ticker ticker, TradeDirection direction, Timeframe timeframe, DateTime expiration, bool ignoreSLTP, decimal? leverage = null) { if (DateTime.Now > expiration) { await component.Channel.SendMessageAsync( "Sorry I can't open position because you tried to click on a expired button."); } else { var exchangeService = (IExchangeService)_services.GetService(typeof(IExchangeService)); var moneyManagementService = (IMoneyManagementService)_services.GetService(typeof(IMoneyManagementService)); var accountService = (IAccountService)_services.GetService(typeof(IAccountService)); var tradingService = (ITradingService)_services.GetService(typeof(ITradingService)); var tradeCommand = new OpenPositionRequest( accountName, await moneyManagementService.GetMoneyMangement(moneyManagement), direction, ticker, initiator, DateTime.UtcNow, ignoreSLTP: ignoreSLTP); var position = await new OpenPositionCommandHandler(exchangeService, accountService, tradingService) .Handle(tradeCommand); var builder = new ComponentBuilder().WithButton("Close Position", $"{Constants.DiscordButtonAction.ClosePosition}{_separator}{position.Identifier}"); await component.Channel.SendMessageAsync(MessengerHelpers.GetPositionMessage(position), components: builder.Build()); } } private string GetClosingPositionMessage(Position position) { return $"Closing : {position.OriginDirection} {position.Open.Ticker} \n" + $"Open Price : {position.Open.Price} \n" + $"Closing Price : {position.Open.Price} \n" + $"Quantity :{position.Open.Quantity} \n" + $"PNL : {position.ProfitAndLoss.Net} $"; } private async Task ClosePosition(SocketMessageComponent component, string[] parameters) { var exchangeService = (IExchangeService)_services.GetService(typeof(IExchangeService)); var accountService = (IAccountService)_services.GetService(typeof(IAccountService)); var tradingService = (ITradingService)_services.GetService(typeof(ITradingService)); await component.RespondAsync("Alright, let met few seconds to close this position"); var position = _tradingService.GetPositionByIdentifier(parameters[1]); var command = new ClosePositionCommand(position); var result = await new ClosePositionCommandHandler(exchangeService, accountService, tradingService).Handle(command); var fields = new List() { new EmbedFieldBuilder { Name = "Direction", Value = position.OriginDirection, IsInline = true }, new EmbedFieldBuilder { Name = "Open Price", Value = $"{position.Open.Price:#.##}", IsInline = true }, new EmbedFieldBuilder { Name = "Quantity", Value = $"{position.Open.Quantity:#.##}", IsInline = true }, new EmbedFieldBuilder { Name = "Pnl", Value = $"{position.ProfitAndLoss.Net:#.##}", IsInline = true }, }; var embed = DiscordHelpers.GetEmbed(position.AccountName, $"Position status is now {result.Status}", fields, position.ProfitAndLoss.Net > 0 ? Color.Green : Color.Red); await component.Channel.SendMessageAsync("", embed: embed); } #endregion #region Out public async Task SendSignal(string message) { var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel; var builder = new ComponentBuilder().WithButton("Open Position", $"openposition{_separator}"); await channel.SendMessageAsync(message, components: builder.Build()); } public async Task SendSignal(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction, Timeframe timeframe) { var expirationDate = DateTime.Now.AddMinutes(_settings.ButtonExpirationMinutes).ToString("G"); var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel; var builder = new ComponentBuilder().WithButton("Open Position", $"{Constants.DiscordButtonAction.OpenPosition}{_separator}{exchange}{_separator}{ticker}{_separator}{direction}{_separator}{timeframe}{_separator}{expirationDate}"); await channel.SendMessageAsync(message, components: builder.Build()); } public async Task SendIncreasePosition(string address, Trade trade, string copyAccountName, Trade? oldTrade = null) { var channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel; var fields = new List() { new EmbedFieldBuilder { Name = "Last size update", Value = $"{trade.Date:s}", IsInline = true }, new EmbedFieldBuilder { Name = "Entry Price", Value = $"{trade.Price:#.##} $", IsInline = true }, new EmbedFieldBuilder { Name = "Quantity", Value = $"{trade.Quantity / trade.Leverage:#.##}", IsInline = true }, new EmbedFieldBuilder { Name = "Leverage", Value = $"x{trade.Leverage:#.##}", IsInline = true } }; if (oldTrade != null) { fields.Add(new EmbedFieldBuilder { Name = "Increasy by", Value = $"{(trade.Quantity - oldTrade.Quantity) / trade.Leverage:#.##} $" }); } var titlePrefix = oldTrade != null ? "Increase " : ""; var builder = new ComponentBuilder(); var moneyManagementService = (IMoneyManagementService)_services.GetService(typeof(IMoneyManagementService)); var moneyManagements = moneyManagementService.GetMoneyMangements(); foreach (var mm in moneyManagements) { var data = new CopyTradeData { Direction = trade.Direction, Ticker = trade.Ticker, AccountName = copyAccountName, ExpirationMinute = 10, Leverage = trade.Leverage, }; data.MoneyManagementName = mm.Name; var encodedData = MiscExtensions.Base64Encode(JsonConvert.SerializeObject(data)); if (oldTrade == null) { builder.WithButton($"Copy with {mm.Name}", $"{Constants.DiscordButtonAction.CopyPosition}{_separator}{encodedData}"); } else { builder.WithButton($"Increase with {mm.Name}", $"{Constants.DiscordButtonAction.CopyPosition}{_separator}{encodedData}"); } } var embed = DiscordHelpers.GetEmbed(address, $"{titlePrefix}{trade.Direction} {trade.Ticker}", fields, trade.Direction == TradeDirection.Long ? Color.Green : Color.Red); await channel.SendMessageAsync("", components: builder.Build(), embed: embed); } public async Task SendPosition(string message, TradingExchanges exchange, Ticker ticker, TradeDirection direction, Timeframe timeframe) { var expirationDate = DateTime.Now.AddMinutes(_settings.ButtonExpirationMinutes).ToString("G"); var channel = _client.GetChannel(_settings.SignalChannelId) as IMessageChannel; var builder = new ComponentBuilder().WithButton("Open Position", $"{Constants.DiscordButtonAction.OpenPosition}{_separator}{exchange}{_separator}{ticker}{_separator}{direction}{_separator}{timeframe}{_separator}{expirationDate}"); await channel.SendMessageAsync(message, components: builder.Build()); } public async Task SendMessage(string message) { var channel = _client.GetChannel(_settings.TradesChannelId) as IMessageChannel; await channel.SendMessageAsync(message); } public async Task SendClosingPosition(Position position) { var channel = _client.GetChannel(_settings.TradesChannelId) as IMessageChannel; await channel.SendMessageAsync(GetClosingPositionMessage(position)); } public async Task SendTradeMessage(string message, bool isBadBehavior = false) { var channel = _client.GetChannel(isBadBehavior ? _settings.TroublesChannelId : _settings.TradesChannelId) as IMessageChannel; await channel.SendMessageAsync(message); } public async Task SendClosedPosition(string address, Trade oldTrade) { var fields = new List() { new EmbedFieldBuilder { Name = "Last size update", Value = $"{oldTrade.Date:s}", IsInline = true }, new EmbedFieldBuilder { Name = "Entry Price", Value = $"{oldTrade.Price:#.##} $", IsInline = true }, new EmbedFieldBuilder { Name = "Quantity", Value = $"{oldTrade.Quantity / oldTrade.Leverage:#.##}", IsInline = true }, new EmbedFieldBuilder { Name = "Leverage", Value = $"x{oldTrade.Leverage:#.##}", IsInline = true } }; var embed = DiscordHelpers.GetEmbed(address, $"Closed {oldTrade.Direction} {oldTrade.Ticker}", fields, oldTrade.Direction == TradeDirection.Long ? Color.DarkGreen : Color.DarkRed); var channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel; await channel.SendMessageAsync("", embed: embed); } public async Task SendDecreasePosition(string address, Trade trade, decimal decreaseAmount) { var fields = new List() { new EmbedFieldBuilder { Name = "Last size update", Value = $"{trade.Date:s}", IsInline = true }, new EmbedFieldBuilder { Name = "Entry Price", Value = $"{trade.Price:#.##} $", IsInline = true }, new EmbedFieldBuilder { Name = "Quantity", Value = $"{trade.Quantity / trade.Leverage:#.##}", IsInline = true }, new EmbedFieldBuilder { Name = "Leverage", Value = $"x{trade.Leverage:#.##}", IsInline = true }, new EmbedFieldBuilder { Name = "Decrease amount", Value = $"{decreaseAmount:#.##} $" } }; var embed = DiscordHelpers.GetEmbed(address, $"Decrease {trade.Direction} {trade.Ticker}", fields, Color.Blue); var channel = _client.GetChannel(_settings.CopyTradingChannelId) as IMessageChannel; await channel.SendMessageAsync("", embed: embed); } public async Task SendPosition(Position position) { var channel = _client.GetChannel(_settings.TradesChannelId) as IMessageChannel; var builder = new ComponentBuilder().WithButton("Close Position", $"{Constants.DiscordButtonAction.ClosePosition}{_separator}{position.Open.ExchangeOrderId}"); await channel.SendMessageAsync(MessengerHelpers.GetPositionMessage(position), components: builder.Build()); } public async Task SendBestTraders(List traders) { var channel = _client.GetChannel(_settings.LeaderboardChannelId) as IMessageChannel; await channel.SendMessageAsync("", embed: DiscordHelpers.GetTradersEmbed(traders, "Leaderboard")); } public async Task SendBadTraders(List traders) { var channel = _client.GetChannel(_settings.NoobiesboardChannelId) as IMessageChannel; await channel.SendMessageAsync("", embed: DiscordHelpers.GetTradersEmbed(traders, "Noobiesboard")); } public async Task SendDowngradedFundingRate(FundingRate fundingRate) { var channel = _client.GetChannel(_settings.FundingRateChannelId) as IMessageChannel; await channel.SendMessageAsync("", embed: DiscordHelpers.GetFundingRateEmbed(fundingRate, "Funding rate new opportunity")); } public Task SendNewTopFundingRate(FundingRate newRate) { var channel = _client.GetChannel(_settings.FundingRateChannelId) as IMessageChannel; return channel.SendMessageAsync("", embed: DiscordHelpers.GetFundingRateEmbed(newRate, "Funding rate new opportunity")); } public Task SendFundingRateUpdate(FundingRate oldRate, FundingRate newRate) { var channel = _client.GetChannel(_settings.FundingRateChannelId) as IMessageChannel; return channel.SendMessageAsync("", embed: DiscordHelpers.GetFundingRateEmbed(newRate, "Funding rate new opportunity", oldRate)); } #endregion public class CopyTradeData { [JsonProperty(PropertyName = "D")] public TradeDirection Direction { get; set; } [JsonProperty(PropertyName = "T")] public Ticker Ticker { get; set; } [JsonProperty(PropertyName = "A")] public string AccountName { get; set; } [JsonProperty(PropertyName = "E")] public int ExpirationMinute { get; set; } [JsonProperty(PropertyName = "L")] public decimal Leverage { get; set; } [JsonProperty(PropertyName = "M")] public string MoneyManagementName { get; internal set; } } } }