Skip to content

Commit c2582d0

Browse files
omgitsjanCopilot
andauthored
Refactored the whole Bot with better messages and a few Tweaks (#97)
* Refactored the whole Bot with better messages and a few Tweaks * Update DiscordBotTests/ServiceTests/Watch2GetherServiceTests.cs Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 00ec135 commit c2582d0

22 files changed

+1432
-1386
lines changed

DiscordBot/DiscordBot.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<PackageId>DiscordBotAI</PackageId>
@@ -29,6 +29,7 @@
2929
<PackageReference Include="DSharpPlus.Interactivity" Version="4.5.1" />
3030
<PackageReference Include="DSharpPlus.SlashCommands" Version="4.5.1" />
3131
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.8" />
32+
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.8" />
3233
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.8" />
3334
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
3435
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.8" />
@@ -37,6 +38,9 @@
3738
</ItemGroup>
3839

3940
<ItemGroup>
41+
<None Update="appsettings.example.json">
42+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
43+
</None>
4044
<None Update="appsettings.json">
4145
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
4246
</None>
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
using DiscordBot.Models;
1+
using System.Threading.Tasks;
2+
using DiscordBot.Models;
23

34
namespace DiscordBot.Interfaces
45
{
56
public interface IOpenWeatherMapService
67
{
7-
public Task<(bool Success, string Message, WeatherData? weatherData)> GetWeatherAsync(string city);
8+
Task<(bool Success, string Message, WeatherData? WeatherData)> GetWeatherAsync(string city);
89
}
9-
}
10+
}

DiscordBot/Program.cs

Lines changed: 176 additions & 138 deletions
Large diffs are not rendered by default.

DiscordBot/Services/CryptoService.cs

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,59 +3,69 @@
33
using Microsoft.Extensions.Logging;
44
using Newtonsoft.Json;
55
using Newtonsoft.Json.Linq;
6+
using System;
7+
using System.Threading.Tasks;
68

79
namespace DiscordBot.Services
810
{
11+
/// <summary>
12+
/// Service for retrieving cryptocurrency prices via a configurable HTTP API.
13+
/// </summary>
914
public class CryptoService(IHttpService httpService, IConfiguration configuration) : ICryptoService
1015
{
1116
/// <summary>
12-
/// Url to the Bitcoin Price Api
17+
/// Base URL for the ByBit API (set via configuration).
1318
/// </summary>
1419
private string? _byBitApiUrl;
1520

1621
/// <summary>
17-
/// Gets the current Price of a given Cryptocurrency. Default = BTC & USDT
22+
/// Retrieves the current price for a specified cryptocurrency and currency pair.
1823
/// </summary>
19-
/// <returns>The current price of given Cryptocurrency as string</returns>
24+
/// <param name="symbol">The cryptocurrency symbol, e.g. BTC (default: BTC).</param>
25+
/// <param name="physicalCurrency">The comparison currency, e.g. USDT (default: USDT).</param>
26+
/// <returns>Tuple indicating success and the price or error message.</returns>
2027
public async Task<Tuple<bool, string>> GetCryptoPriceAsync(string symbol = "BTC", string physicalCurrency = "USDT")
2128
{
2229
_byBitApiUrl = configuration["ByBit:ApiUrl"] ?? string.Empty;
23-
symbol = symbol.ToUpper();
30+
symbol = symbol.ToUpperInvariant();
31+
2432
if (string.IsNullOrEmpty(_byBitApiUrl))
2533
{
26-
const string errorMessage =
27-
"No ByBit Api Url was provided, please contact the Developer to add a valid Api Url!";
28-
Program.Log($"{nameof(GetCryptoPriceAsync)}: " + errorMessage, LogLevel.Error);
29-
return new Tuple<bool, string>(false, errorMessage);
34+
const string errorMessage = "No ByBit API URL configured. Please contact the developer to add a valid API URL.";
35+
Program.Log($"{nameof(GetCryptoPriceAsync)}: {errorMessage}", LogLevel.Error);
36+
return Tuple.Create(false, errorMessage);
3037
}
3138

32-
string requestUrl = _byBitApiUrl + symbol + physicalCurrency;
33-
Console.WriteLine("Sending Reqeust to: " + requestUrl);
34-
HttpResponse response = await httpService.GetResponseFromUrl(requestUrl);
39+
string requestUrl = $"{_byBitApiUrl}{symbol}{physicalCurrency}";
40+
Program.Log($"Requesting: {requestUrl}", LogLevel.Debug);
3541

42+
HttpResponse response = await httpService.GetResponseFromUrl(requestUrl);
3643
if (!response.IsSuccessStatusCode)
3744
{
38-
return new Tuple<bool, string>(false, response.Content ?? "");
45+
return Tuple.Create(false, response.Content ?? "API error (no content).");
3946
}
4047

4148
try
4249
{
4350
JObject json = JObject.Parse(response.Content ?? "{}");
44-
string? respString = json["result"]?["list"]?[0]?["lastPrice"]?.Value<string>();
45-
bool success = respString != null;
51+
string? priceString = json["result"]?["list"]?[0]?["lastPrice"]?.Value<string>();
52+
bool success = priceString != null;
4653

4754
if (!success)
48-
{
49-
respString = $"Could not fetch price of {symbol}...";
50-
}
55+
priceString = $"Could not fetch price for {symbol}.";
5156

52-
Program.Log($"{nameof(GetCryptoPriceAsync)}: {respString} - {success}", LogLevel.Information);
53-
return new Tuple<bool, string>(success, respString ?? throw new Exception());
57+
Program.Log($"{nameof(GetCryptoPriceAsync)}: {priceString} (success={success})", LogLevel.Information);
58+
return Tuple.Create(success, priceString ?? throw new Exception("No response string."));
5459
}
5560
catch (JsonReaderException ex)
5661
{
57-
Program.Log($"{nameof(GetCryptoPriceAsync)}: " + ex.Message, LogLevel.Error);
58-
return new Tuple<bool, string>(false, $"Could not fetch price of {symbol}...");
62+
Program.Log($"{nameof(GetCryptoPriceAsync)}: JSON parse error: {ex.Message}", LogLevel.Error);
63+
return Tuple.Create(false, $"Could not fetch price for {symbol} (invalid API response).");
64+
}
65+
catch (Exception ex)
66+
{
67+
Program.Log($"{nameof(GetCryptoPriceAsync)}: Unexpected error: {ex.Message}", LogLevel.Error);
68+
return Tuple.Create(false, $"Could not fetch price for {symbol} (unexpected error).");
5969
}
6070
}
6171
}
Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,46 @@
11
using DiscordBot.Interfaces;
22
using Microsoft.Extensions.Logging;
33
using Newtonsoft.Json.Linq;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Threading.Tasks;
48

59
namespace DiscordBot.Services
610
{
11+
/// <summary>
12+
/// Provides helper utilities, such as loading and returning random developer excuses.
13+
/// </summary>
714
public class HelperService : IHelperService
815
{
916
private readonly List<string> _developerExcuses = [];
1017
private readonly ILogger<HelperService> _logger;
1118

19+
/// <summary>
20+
/// Constructs the HelperService, loading developer excuses from the default location.
21+
/// </summary>
1222
public HelperService(ILogger<HelperService> logger)
1323
{
1424
_logger = logger;
1525
LoadDeveloperExcuses(null);
1626
}
1727

28+
/// <summary>
29+
/// Constructs the HelperService, loading developer excuses from a custom file path.
30+
/// </summary>
1831
public HelperService(ILogger<HelperService> logger, string? excusesFilePath)
1932
{
2033
_logger = logger;
2134
LoadDeveloperExcuses(excusesFilePath);
2235
}
2336

37+
/// <summary>
38+
/// Loads developer excuses from a JSON file. If no path is provided, the default file is used.
39+
/// </summary>
40+
/// <param name="excusesFilePath">Optional custom path to the excuses file.</param>
2441
private void LoadDeveloperExcuses(string? excusesFilePath)
2542
{
2643
string filePath = excusesFilePath ?? Path.Combine(Directory.GetCurrentDirectory(), "Data", "excuses.json");
27-
2844
if (!File.Exists(filePath))
2945
{
3046
_logger.LogError("Developer excuses file not found at: {FilePath}", filePath);
@@ -35,24 +51,30 @@ private void LoadDeveloperExcuses(string? excusesFilePath)
3551
{
3652
string jsonContent = File.ReadAllText(filePath);
3753
JObject json = JObject.Parse(jsonContent);
38-
_developerExcuses.AddRange(json["en"]?.ToObject<List<string>>() ?? throw new Exception());
54+
var excuses = json["en"]?.ToObject<List<string>>() ?? [];
55+
_developerExcuses.AddRange(excuses);
56+
if (_developerExcuses.Count == 0)
57+
_logger.LogWarning("Developer excuses file loaded, but no excuses found in 'en' array.");
3958
}
4059
catch (Exception ex)
4160
{
4261
_logger.LogError(ex, "Error loading developer excuses from file: {FilePath}", filePath);
4362
}
4463
}
4564

65+
/// <summary>
66+
/// Returns a random developer excuse from the loaded list.
67+
/// </summary>
68+
/// <returns>Random developer excuse as a string, or an error message if unavailable.</returns>
4669
public Task<string> GetRandomDeveloperExcuseAsync()
4770
{
4871
if (_developerExcuses.Count == 0)
4972
{
50-
return Task.FromResult("Could not fetch a Developer excuse. Please check the configuration.");
73+
return Task.FromResult("Could not fetch a developer excuse. Please check the configuration or file content.");
5174
}
52-
53-
Random random = new();
75+
var random = new Random();
5476
int index = random.Next(_developerExcuses.Count);
5577
return Task.FromResult(_developerExcuses[index]);
5678
}
5779
}
58-
}
80+
}

DiscordBot/Services/HttpService.cs

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,89 @@
22
using DiscordBot.Interfaces;
33
using Microsoft.Extensions.Logging;
44
using RestSharp;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Threading.Tasks;
58

69
namespace DiscordBot.Services
710
{
11+
/// <summary>
12+
/// Provides HTTP utilities for making REST API requests and returning simplified results.
13+
/// </summary>
814
public class HttpService(IRestClient httpClient) : IHttpService
915
{
1016
/// <summary>
11-
/// Gets the response from a URL and handles errors
17+
/// Sends an HTTP request to the specified resource and returns the response.
18+
/// Supports custom headers, HTTP methods, and optional JSON bodies.
1219
/// </summary>
13-
/// <returns>The current price from bitcoin as BTC-USDT string</returns>
14-
public async Task<HttpResponse> GetResponseFromUrl(string resource, Method method = Method.Get,
15-
string? errorMessage = null, List<KeyValuePair<string, string>>? headers = null, object? jsonBody = null)
20+
/// <param name="resource">The target URL or endpoint.</param>
21+
/// <param name="method">HTTP method (default: GET).</param>
22+
/// <param name="errorMessage">Optional custom error message if the request fails.</param>
23+
/// <param name="headers">Optional list of HTTP headers.</param>
24+
/// <param name="jsonBody">Optional JSON body for POST/PUT requests.</param>
25+
/// <returns>HttpResponse containing status and content or error.</returns>
26+
public async Task<HttpResponse> GetResponseFromUrl(
27+
string resource,
28+
Method method = Method.Get,
29+
string? errorMessage = null,
30+
List<KeyValuePair<string, string>>? headers = null,
31+
object? jsonBody = null)
1632
{
17-
RestRequest request = new(resource, method);
33+
var request = new RestRequest(resource, method);
1834

19-
if (headers != null && headers.Count != 0)
35+
// Add headers if provided
36+
if (headers != null && headers.Count > 0)
2037
{
21-
headers.ForEach(header => request.AddHeader(header.Key, header.Value));
38+
foreach (var header in headers)
39+
request.AddHeader(header.Key, header.Value);
2240
}
2341

42+
// Attach JSON body if provided
2443
if (jsonBody != null)
2544
{
2645
request.AddJsonBody(jsonBody);
2746
}
2847

29-
RestResponse response = new();
30-
31-
// Send the HTTP request asynchronously and await the response.
48+
RestResponse response;
3249
try
3350
{
51+
// Execute the request and await the response
3452
response = await httpClient.ExecuteAsync(request);
3553
}
3654
catch (Exception e)
3755
{
38-
response.IsSuccessStatusCode = false;
39-
response.ErrorMessage = $"({nameof(GetResponseFromUrl)}): Unknown error occurred" + e.Message;
40-
response.ErrorException = e;
41-
response.StatusCode = HttpStatusCode.InternalServerError;
56+
// Catch all exceptions and build a failed response
57+
string failMessage = $"({nameof(GetResponseFromUrl)}): Unknown error: {e.Message}";
58+
Program.Log(failMessage, LogLevel.Error);
59+
return new HttpResponse(false, failMessage);
4260
}
4361

44-
string? content = response.Content;
45-
62+
// If the HTTP response was successful, return its content
4663
if (response.IsSuccessStatusCode)
4764
{
48-
return new HttpResponse(response.IsSuccessStatusCode, content);
65+
return new HttpResponse(true, response.Content);
4966
}
5067

51-
content = $"StatusCode: {response.StatusCode} | {errorMessage ?? response.ErrorMessage}";
68+
// If failed, build a detailed error message for logging and the caller
69+
string content = $"StatusCode: {response.StatusCode} | {errorMessage ?? response.ErrorMessage}";
5270
Program.Log(content, LogLevel.Error);
53-
54-
return new HttpResponse(response.IsSuccessStatusCode, content);
71+
return new HttpResponse(false, content);
5572
}
5673
}
5774

75+
/// <summary>
76+
/// A simple wrapper for HTTP response results (success + content).
77+
/// </summary>
5878
public class HttpResponse(bool isSuccessStatusCode, string? content)
5979
{
80+
/// <summary>
81+
/// Indicates whether the HTTP request was successful (2xx).
82+
/// </summary>
6083
public bool IsSuccessStatusCode { get; set; } = isSuccessStatusCode;
84+
85+
/// <summary>
86+
/// The response content or error message.
87+
/// </summary>
6188
public string? Content { get; set; } = content;
6289
}
63-
}
90+
}

0 commit comments

Comments
 (0)