From 1f2780d52a13ee0abc4848608b040449cdec90cd Mon Sep 17 00:00:00 2001 From: cryptooda Date: Mon, 9 Jun 2025 01:04:02 +0700 Subject: [PATCH] Add webhook --- .../Controllers/UserController.cs | 13 ++++ src/Managing.Api/appsettings.json | 3 + .../Services/IMessengerService.cs | 3 +- .../Services/IUserService.cs | 1 + .../Services/IWebhookService.cs | 8 +++ src/Managing.Application/Bots/TradingBot.cs | 30 ++++----- .../Shared/MessengerService.cs | 26 +++++++- .../Shared/WebhookService.cs | 62 +++++++++++++++++++ src/Managing.Application/Users/UserService.cs | 17 +++++ src/Managing.Bootstrap/ApiBootstrap.cs | 1 + src/Managing.Bootstrap/WorkersBootstrap.cs | 4 ++ src/Managing.Domain/Users/User.cs | 3 +- .../MongoDb/Collections/UserDto.cs | 1 + .../MongoDb/MongoMappers.cs | 2 + .../src/generated/ManagingApi.ts | 40 ++++++++++++ 15 files changed, 195 insertions(+), 19 deletions(-) create mode 100644 src/Managing.Application.Abstractions/Services/IWebhookService.cs create mode 100644 src/Managing.Application/Shared/WebhookService.cs diff --git a/src/Managing.Api/Controllers/UserController.cs b/src/Managing.Api/Controllers/UserController.cs index 0a8ca81..02b8cbf 100644 --- a/src/Managing.Api/Controllers/UserController.cs +++ b/src/Managing.Api/Controllers/UserController.cs @@ -87,5 +87,18 @@ public class UserController : BaseController var updatedUser = await _userService.UpdateAvatarUrl(user, avatarUrl); return Ok(updatedUser); } + + /// + /// Updates the Telegram channel for the current user. + /// + /// The new Telegram channel to set. + /// The updated user with the new Telegram channel. + [HttpPut("telegram-channel")] + public async Task> UpdateTelegramChannel([FromBody] string telegramChannel) + { + var user = await GetUser(); + var updatedUser = await _userService.UpdateTelegramChannel(user, telegramChannel); + return Ok(updatedUser); + } } \ No newline at end of file diff --git a/src/Managing.Api/appsettings.json b/src/Managing.Api/appsettings.json index 1f8c47b..8e69e2f 100644 --- a/src/Managing.Api/appsettings.json +++ b/src/Managing.Api/appsettings.json @@ -24,6 +24,9 @@ "Web3Proxy": { "BaseUrl": "http://localhost:4111" }, + "N8n": { + "WebhookUrl": "https://n8n.kaigen.managing.live/webhook/fa9308b6-983b-42ec-b085-71599d655951" + }, "Sentry": { "Dsn": "https://698e00d7cb404b049aff3881e5a47f6b@bugcenter.apps.managing.live/1", "MinimumEventLevel": "Error", diff --git a/src/Managing.Application.Abstractions/Services/IMessengerService.cs b/src/Managing.Application.Abstractions/Services/IMessengerService.cs index 0f88e14..2ed4277 100644 --- a/src/Managing.Application.Abstractions/Services/IMessengerService.cs +++ b/src/Managing.Application.Abstractions/Services/IMessengerService.cs @@ -1,5 +1,6 @@ ο»Ώusing Managing.Domain.Statistics; using Managing.Domain.Trades; +using Managing.Domain.Users; using static Managing.Common.Enums; namespace Managing.Application.Abstractions.Services; @@ -12,7 +13,7 @@ public interface IMessengerService Task SendPosition(Position position); Task SendClosingPosition(Position position); Task SendMessage(string v); - Task SendTradeMessage(string message, bool isBadBehavior = false); + Task SendTradeMessage(string message, bool isBadBehavior = false, User user = null); Task SendIncreasePosition(string address, Trade trade, string copyAccountName, Trade? oldTrade = null); Task SendClosedPosition(string address, Trade oldTrade); Task SendDecreasePosition(string address, Trade newTrade, decimal decreaseAmount); diff --git a/src/Managing.Application.Abstractions/Services/IUserService.cs b/src/Managing.Application.Abstractions/Services/IUserService.cs index ca09541..258534a 100644 --- a/src/Managing.Application.Abstractions/Services/IUserService.cs +++ b/src/Managing.Application.Abstractions/Services/IUserService.cs @@ -8,5 +8,6 @@ public interface IUserService Task GetUserByAddressAsync(string address); Task UpdateAgentName(User user, string agentName); Task UpdateAvatarUrl(User user, string avatarUrl); + Task UpdateTelegramChannel(User user, string telegramChannel); User GetUser(string name); } diff --git a/src/Managing.Application.Abstractions/Services/IWebhookService.cs b/src/Managing.Application.Abstractions/Services/IWebhookService.cs new file mode 100644 index 0000000..5d92d11 --- /dev/null +++ b/src/Managing.Application.Abstractions/Services/IWebhookService.cs @@ -0,0 +1,8 @@ +using Managing.Domain.Users; + +namespace Managing.Application.Abstractions.Services; + +public interface IWebhookService +{ + Task SendTradeNotification(User user, string message, bool isBadBehavior = false); +} \ No newline at end of file diff --git a/src/Managing.Application/Bots/TradingBot.cs b/src/Managing.Application/Bots/TradingBot.cs index ba34312..3a22ce2 100644 --- a/src/Managing.Application/Bots/TradingBot.cs +++ b/src/Managing.Application/Bots/TradingBot.cs @@ -1163,7 +1163,7 @@ public class TradingBot : Bot, ITradingBot { if (!Config.IsForBacktest) { - await MessengerService.SendTradeMessage(message, isBadBehavior); + await MessengerService.SendTradeMessage(message, isBadBehavior, Account?.User); } } @@ -1322,13 +1322,14 @@ public class TradingBot : Bot, ITradingBot var protectedIsForBacktest = Config.IsForBacktest; var protectedName = Config.Name; - // Log the configuration update - await LogInformation($"Updating bot configuration. Previous config: " + - $"Balance: {Config.BotTradingBalance}, " + - $"MaxTime: {Config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " + - $"FlipOnlyProfit: {Config.FlipOnlyWhenInProfit}, " + - $"Cooldown: {Config.CooldownPeriod}, " + - $"MaxLoss: {Config.MaxLossStreak}"); + // Log the configuration update (before changing anything) + await LogInformation("βš™οΈ **Configuration Update**\n" + + "πŸ“Š **Previous Settings:**\n" + + $"πŸ’° Balance: ${Config.BotTradingBalance:F2}\n" + + $"⏱️ Max Time: {(Config.MaxPositionTimeHours?.ToString() + "h" ?? "Disabled")}\n" + + $"πŸ“ˆ Flip Only in Profit: {(Config.FlipOnlyWhenInProfit ? "βœ…" : "❌")}\n" + + $"⏳ Cooldown: {Config.CooldownPeriod} candles\n" + + $"πŸ“‰ Max Loss Streak: {Config.MaxLossStreak}"); // Update the configuration Config = newConfig; @@ -1351,12 +1352,13 @@ public class TradingBot : Bot, ITradingBot LoadScenario(Config.ScenarioName); } - await LogInformation($"Bot configuration updated successfully. New config: " + - $"Balance: {Config.BotTradingBalance}, " + - $"MaxTime: {Config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " + - $"FlipOnlyProfit: {Config.FlipOnlyWhenInProfit}, " + - $"Cooldown: {Config.CooldownPeriod}, " + - $"MaxLoss: {Config.MaxLossStreak}"); + await LogInformation("βœ… **Configuration Applied**\n" + + "πŸ”§ **New Settings:**\n" + + $"πŸ’° Balance: ${Config.BotTradingBalance:F2}\n" + + $"⏱️ Max Time: {(Config.MaxPositionTimeHours?.ToString() + "h" ?? "Disabled")}\n" + + $"πŸ“ˆ Flip Only in Profit: {(Config.FlipOnlyWhenInProfit ? "βœ…" : "❌")}\n" + + $"⏳ Cooldown: {Config.CooldownPeriod} candles\n" + + $"πŸ“‰ Max Loss Streak: {Config.MaxLossStreak}"); // Save the updated configuration as backup if (!Config.IsForBacktest) diff --git a/src/Managing.Application/Shared/MessengerService.cs b/src/Managing.Application/Shared/MessengerService.cs index 8d1843d..5b62f0e 100644 --- a/src/Managing.Application/Shared/MessengerService.cs +++ b/src/Managing.Application/Shared/MessengerService.cs @@ -2,16 +2,21 @@ using Managing.Common; using Managing.Domain.Statistics; using Managing.Domain.Trades; +using Managing.Domain.Users; namespace Managing.Application.Shared; public class MessengerService : IMessengerService { private readonly IDiscordService _discordService; + private readonly IWebhookService _webhookService; + private readonly IUserService _userService; - public MessengerService(IDiscordService discordService) + public MessengerService(IDiscordService discordService, IWebhookService webhookService, IUserService userService) { _discordService = discordService; + _webhookService = webhookService; + _userService = userService; } public async Task SendClosedPosition(string address, Trade oldTrade) @@ -50,9 +55,24 @@ public class MessengerService : IMessengerService await _discordService.SendSignal(message, exchange, ticker, direction, timeframe); } - public async Task SendTradeMessage(string message, bool isBadBehavior = false) + public async Task SendTradeMessage(string message, bool isBadBehavior = false, User user = null) { - await _discordService.SendTradeMessage(message, isBadBehavior); + // Always send to Discord + try + { + await _discordService.SendTradeMessage(message, isBadBehavior); + } + catch (Exception e) + { + Console.WriteLine(e); + } + + // If user is provided, also send to webhook + if (user != null) + { + user = _userService.GetUser(user.Name); + await _webhookService.SendTradeNotification(user, message, isBadBehavior); + } } public async Task SendBestTraders(List traders) diff --git a/src/Managing.Application/Shared/WebhookService.cs b/src/Managing.Application/Shared/WebhookService.cs new file mode 100644 index 0000000..1d392cd --- /dev/null +++ b/src/Managing.Application/Shared/WebhookService.cs @@ -0,0 +1,62 @@ +using System.Net.Http.Json; +using Managing.Application.Abstractions.Services; +using Managing.Domain.Users; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Managing.Application.Shared; + +public class WebhookService : IWebhookService +{ + private readonly HttpClient _httpClient; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public WebhookService(HttpClient httpClient, IConfiguration configuration, ILogger logger) + { + _httpClient = httpClient; + _configuration = configuration; + _logger = logger; + } + + public async Task SendTradeNotification(User user, string message, bool isBadBehavior = false) + { + try + { + // Get the n8n webhook URL from configuration + var webhookUrl = _configuration["N8n:WebhookUrl"]; + if (string.IsNullOrEmpty(webhookUrl)) + { + _logger.LogWarning("N8n webhook URL not configured, skipping webhook notification"); + return; + } + + // Prepare the payload for n8n webhook + var payload = new + { + message = message, + isBadBehavior = isBadBehavior, + timestamp = DateTime.UtcNow, + type = "trade_notification", + telegramChannel = user.TelegramChannel + }; + + // Send the webhook notification + var response = await _httpClient.PostAsJsonAsync(webhookUrl, payload); + + if (response.IsSuccessStatusCode) + { + _logger.LogInformation($"Successfully sent webhook notification for user {user.Name}"); + } + else + { + _logger.LogWarning($"Failed to send webhook notification. Status: {response.StatusCode}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error sending webhook notification for user {user.Name}: {ex.Message}"); + // Don't throw - webhook failures shouldn't break the main flow + } + } +} \ No newline at end of file diff --git a/src/Managing.Application/Users/UserService.cs b/src/Managing.Application/Users/UserService.cs index ac1ea6b..431d415 100644 --- a/src/Managing.Application/Users/UserService.cs +++ b/src/Managing.Application/Users/UserService.cs @@ -165,4 +165,21 @@ public class UserService : IUserService await _userRepository.UpdateUser(user); return user; } + + public async Task UpdateTelegramChannel(User user, string telegramChannel) + { + // Validate Telegram channel format (must start with @ and contain only allowed characters) + if (!string.IsNullOrEmpty(telegramChannel)) + { + string pattern = @"^@[a-zA-Z0-9_]{5,32}$"; + if (!Regex.IsMatch(telegramChannel, pattern)) + { + throw new Exception("Invalid Telegram channel format. Must start with @ and be 5-32 characters long, containing only letters, numbers, and underscores."); + } + } + + user.TelegramChannel = telegramChannel; + await _userRepository.UpdateUser(user); + return user; + } } \ No newline at end of file diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs index dfd1d50..5262677 100644 --- a/src/Managing.Bootstrap/ApiBootstrap.cs +++ b/src/Managing.Bootstrap/ApiBootstrap.cs @@ -94,6 +94,7 @@ public static class ApiBootstrap services.AddSingleton(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); return services; } diff --git a/src/Managing.Bootstrap/WorkersBootstrap.cs b/src/Managing.Bootstrap/WorkersBootstrap.cs index f99feaa..7fc0a01 100644 --- a/src/Managing.Bootstrap/WorkersBootstrap.cs +++ b/src/Managing.Bootstrap/WorkersBootstrap.cs @@ -131,9 +131,13 @@ public static class WorkersBootstrap services.Configure(configuration.GetSection("Web3Proxy")); services.AddTransient(); + // Http Clients + services.AddHttpClient(); + // Messengers services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/Managing.Domain/Users/User.cs b/src/Managing.Domain/Users/User.cs index 62e7416..aeb9bfd 100644 --- a/src/Managing.Domain/Users/User.cs +++ b/src/Managing.Domain/Users/User.cs @@ -8,4 +8,5 @@ public class User public List Accounts { get; set; } public string AgentName { get; set; } public string AvatarUrl { get; set; } -} + public string TelegramChannel { get; set; } +} \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/MongoDb/Collections/UserDto.cs b/src/Managing.Infrastructure.Database/MongoDb/Collections/UserDto.cs index 74a2221..cc35b7d 100644 --- a/src/Managing.Infrastructure.Database/MongoDb/Collections/UserDto.cs +++ b/src/Managing.Infrastructure.Database/MongoDb/Collections/UserDto.cs @@ -9,4 +9,5 @@ public class UserDto : Document public string Name { get; set; } public string AgentName { get; set; } public string AvatarUrl { get; set; } + public string TelegramChannel { get; set; } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs b/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs index f2d6f15..c89d1ca 100644 --- a/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs +++ b/src/Managing.Infrastructure.Database/MongoDb/MongoMappers.cs @@ -534,6 +534,7 @@ public static class MongoMappers Name = user.Name, AgentName = user.AgentName, AvatarUrl = user.AvatarUrl, + TelegramChannel = user.TelegramChannel }; } @@ -544,6 +545,7 @@ public static class MongoMappers Name = user.Name, AgentName = user.AgentName, AvatarUrl = user.AvatarUrl, + TelegramChannel = user.TelegramChannel }; } diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index 26ebb59..28f6e76 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -2447,6 +2447,45 @@ export class UserClient extends AuthorizedApiBase { } return Promise.resolve(null as any); } + + user_UpdateTelegramChannel(telegramChannel: string): Promise { + let url_ = this.baseUrl + "/User/telegram-channel"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(telegramChannel); + + let options_: RequestInit = { + body: content_, + method: "PUT", + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processUser_UpdateTelegramChannel(_response); + }); + } + + protected processUser_UpdateTelegramChannel(response: Response): Promise { + const status = response.status; + let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); }; + if (status === 200) { + return response.text().then((_responseText) => { + let result200: any = null; + result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as User; + return result200; + }); + } else if (status !== 200 && status !== 204) { + return response.text().then((_responseText) => { + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + }); + } + return Promise.resolve(null as any); + } } export class WorkflowClient extends AuthorizedApiBase { @@ -2644,6 +2683,7 @@ export interface User { accounts?: Account[] | null; agentName?: string | null; avatarUrl?: string | null; + telegramChannel?: string | null; } export interface Balance {