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 {