Skip to content

Commit f33fed4

Browse files
HandyS11claude
andcommitted
refactor(discord): extract shared poster boilerplate into DiscordChannelMessenger
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent fe6b8d1 commit f33fed4

7 files changed

Lines changed: 160 additions & 202 deletions

File tree

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
1111
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
1212
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.9" />
13+
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
1314
<PackageVersion Include="Persistord.Core" Version="1.0.0-beta.1" />
1415
<PackageVersion Include="RustPlusApi" Version="2.0.0-beta.3" />
1516
<PackageVersion Include="RustPlusApi.Fcm" Version="2.0.0-beta.3" />
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
using Discord;
2+
using Discord.Net;
3+
using Discord.WebSocket;
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace RustPlusBot.Discord.Posting;
7+
8+
/// <summary>Shared Discord channel post/edit boilerplate: fetch, options, self-heal, broad-catch.</summary>
9+
public static class DiscordChannelMessenger
10+
{
11+
/// <summary>
12+
/// Edits the message by id (self-healing on 404 by reposting) or posts a new one.
13+
/// Returns the message id, or null on failure.
14+
/// </summary>
15+
/// <param name="client">The Discord socket client.</param>
16+
/// <param name="channelId">The target channel id.</param>
17+
/// <param name="messageId">The existing message id to edit, or null to post a new one.</param>
18+
/// <param name="embed">The embed to post or update.</param>
19+
/// <param name="components">The message components to post or update.</param>
20+
/// <param name="logger">The caller's logger.</param>
21+
/// <param name="cancellationToken">The cancellation token.</param>
22+
public static async Task<ulong?> EnsureAsync(
23+
DiscordSocketClient client,
24+
ulong channelId,
25+
ulong? messageId,
26+
Embed embed,
27+
MessageComponent components,
28+
ILogger logger,
29+
CancellationToken cancellationToken)
30+
{
31+
try
32+
{
33+
var options = new RequestOptions
34+
{
35+
CancelToken = cancellationToken
36+
};
37+
if (await client.GetChannelAsync(channelId, options).ConfigureAwait(false)
38+
is not ITextChannel channel)
39+
{
40+
return null;
41+
}
42+
43+
if (messageId is { } id)
44+
{
45+
// Inner try: some Discord.Net versions THROW (HttpException 404/Unknown Message)
46+
// rather than return null for a deleted message. Catch it and fall through to repost
47+
// so the self-heal path always runs.
48+
try
49+
{
50+
var existing = await channel.GetMessageAsync(id, options: options).ConfigureAwait(false);
51+
if (existing is IUserMessage userMessage)
52+
{
53+
await userMessage.ModifyAsync(m =>
54+
{
55+
m.Embed = embed;
56+
m.Components = components;
57+
}, options).ConfigureAwait(false);
58+
return userMessage.Id;
59+
}
60+
61+
// Message was deleted (returned null / not a user message); fall through to repost.
62+
}
63+
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound)
64+
{
65+
// Deleted/unknown message; fall through to repost and return the new id.
66+
#pragma warning disable CA1848, CA1873 // Use LoggerMessage delegates / avoid expensive log-arg evaluation — plain logger.Log is fine for a shared helper (no source-gen partial context); ulong boxing is negligible vs. the caught exception.
67+
logger.LogDebug(ex, "Embed {MessageId} in channel {ChannelId} was deleted; reposting.", id,
68+
channelId);
69+
#pragma warning restore CA1848, CA1873
70+
}
71+
}
72+
73+
var posted = await channel
74+
.SendMessageAsync(embed: embed, options: options, components: components)
75+
.ConfigureAwait(false);
76+
return posted.Id;
77+
}
78+
catch (OperationCanceledException)
79+
{
80+
throw; // Shutdown — let the loop unwind.
81+
}
82+
#pragma warning disable CA1031 // Broad catch: a Discord hiccup must not crash the relay; report failure as null.
83+
catch (Exception ex)
84+
#pragma warning restore CA1031
85+
{
86+
#pragma warning disable CA1848, CA1873 // Use LoggerMessage delegates / avoid expensive log-arg evaluation — plain logger.Log is fine for a shared helper (no source-gen partial context); ulong boxing is negligible vs. the caught exception.
87+
logger.LogWarning(ex, "Posting/editing an embed in channel {ChannelId} failed.", channelId);
88+
#pragma warning restore CA1848, CA1873
89+
return null;
90+
}
91+
}
92+
93+
/// <summary>Posts an embed fire-and-forget; Discord hiccups are logged and swallowed.</summary>
94+
/// <param name="client">The Discord socket client.</param>
95+
/// <param name="channelId">The target channel id.</param>
96+
/// <param name="embed">The embed to post.</param>
97+
/// <param name="logger">The caller's logger.</param>
98+
/// <param name="cancellationToken">The cancellation token.</param>
99+
public static async Task PostAsync(
100+
DiscordSocketClient client,
101+
ulong channelId,
102+
Embed embed,
103+
ILogger logger,
104+
CancellationToken cancellationToken)
105+
{
106+
try
107+
{
108+
var options = new RequestOptions
109+
{
110+
CancelToken = cancellationToken
111+
};
112+
if (await client.GetChannelAsync(channelId, options).ConfigureAwait(false) is not ITextChannel channel)
113+
{
114+
return;
115+
}
116+
117+
await channel.SendMessageAsync(embed: embed, options: options).ConfigureAwait(false);
118+
}
119+
catch (OperationCanceledException)
120+
{
121+
throw; // Cancellation (shutdown) is not a post failure; let the relay loop unwind cleanly.
122+
}
123+
#pragma warning disable CA1031 // Broad catch: a Discord hiccup must not crash the relay.
124+
catch (Exception ex)
125+
#pragma warning restore CA1031
126+
{
127+
#pragma warning disable CA1848, CA1873 // Use LoggerMessage delegates / avoid expensive log-arg evaluation — plain logger.Log is fine for a shared helper (no source-gen partial context); ulong boxing is negligible vs. the caught exception.
128+
logger.LogWarning(ex, "Posting an embed to channel {ChannelId} failed.", channelId);
129+
#pragma warning restore CA1848, CA1873
130+
}
131+
}
132+
}

src/RustPlusBot.Discord/RustPlusBot.Discord.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<ItemGroup>
1010
<PackageReference Include="Discord.Net" />
1111
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
12+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
1213
</ItemGroup>
1314

1415
</Project>

src/RustPlusBot.Features.Alarms/Posting/DiscordAlarmChannelPoster.cs

Lines changed: 10 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
using Discord.Net;
1+
using Discord;
22
using Discord.WebSocket;
33
using Microsoft.Extensions.Logging;
4+
using RustPlusBot.Discord.Posting;
45

56
namespace RustPlusBot.Features.Alarms.Posting;
67

@@ -12,89 +13,34 @@ internal sealed partial class DiscordAlarmChannelPoster(
1213
ILogger<DiscordAlarmChannelPoster> logger) : IAlarmChannelPoster
1314
{
1415
/// <inheritdoc />
15-
public async Task<ulong?> EnsureAsync(
16+
public Task<ulong?> EnsureAsync(
1617
ulong channelId,
1718
ulong? messageId,
18-
global::Discord.Embed embed,
19-
global::Discord.MessageComponent components,
19+
Embed embed,
20+
MessageComponent components,
2021
CancellationToken cancellationToken)
21-
{
22-
try
23-
{
24-
var options = new global::Discord.RequestOptions
25-
{
26-
CancelToken = cancellationToken
27-
};
28-
if (await client.GetChannelAsync(channelId, options).ConfigureAwait(false)
29-
is not global::Discord.ITextChannel channel)
30-
{
31-
return null;
32-
}
33-
34-
if (messageId is { } id)
35-
{
36-
// Inner try: some Discord.Net versions THROW (HttpException 404/Unknown Message)
37-
// rather than return null for a deleted message. Catch it and fall through to repost
38-
// so the self-heal path always runs.
39-
try
40-
{
41-
var existing = await channel.GetMessageAsync(id, options: options).ConfigureAwait(false);
42-
if (existing is global::Discord.IUserMessage userMessage)
43-
{
44-
await userMessage.ModifyAsync(m =>
45-
{
46-
m.Embed = embed;
47-
m.Components = components;
48-
}, options).ConfigureAwait(false);
49-
return userMessage.Id;
50-
}
51-
52-
// Message was deleted (returned null / not a user message); fall through to repost.
53-
}
54-
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound)
55-
{
56-
// Deleted/unknown message; fall through to repost and return the new id.
57-
LogMessageMissing(logger, ex, channelId, id);
58-
}
59-
}
60-
61-
var posted = await channel
62-
.SendMessageAsync(embed: embed, options: options, components: components)
63-
.ConfigureAwait(false);
64-
return posted.Id;
65-
}
66-
catch (OperationCanceledException)
67-
{
68-
throw; // Shutdown — let the loop unwind.
69-
}
70-
#pragma warning disable CA1031 // Broad catch: a Discord hiccup must not crash the relay; report failure as null.
71-
catch (Exception ex)
72-
#pragma warning restore CA1031
73-
{
74-
LogEnsureFailed(logger, ex, channelId);
75-
return null;
76-
}
77-
}
22+
=> DiscordChannelMessenger.EnsureAsync(client, channelId, messageId, embed, components, logger,
23+
cancellationToken);
7824

7925
/// <inheritdoc />
8026
public async Task SendEveryonePingAsync(ulong channelId, string content, CancellationToken cancellationToken)
8127
{
8228
try
8329
{
84-
var options = new global::Discord.RequestOptions
30+
var options = new RequestOptions
8531
{
8632
CancelToken = cancellationToken
8733
};
8834
if (await client.GetChannelAsync(channelId, options).ConfigureAwait(false)
89-
is not global::Discord.ITextChannel channel)
35+
is not ITextChannel channel)
9036
{
9137
return;
9238
}
9339

9440
await channel.SendMessageAsync(
9541
content,
9642
options: options,
97-
allowedMentions: global::Discord.AllowedMentions.All)
43+
allowedMentions: AllowedMentions.All)
9844
.ConfigureAwait(false);
9945
}
10046
catch (OperationCanceledException)
@@ -109,14 +55,6 @@ await channel.SendMessageAsync(
10955
}
11056
}
11157

112-
[LoggerMessage(Level = LogLevel.Warning, Message = "Posting/editing an alarm embed in channel {ChannelId} failed.")]
113-
private static partial void LogEnsureFailed(ILogger logger, Exception exception, ulong channelId);
114-
115-
[LoggerMessage(Level = LogLevel.Debug,
116-
Message = "Alarm embed {MessageId} in channel {ChannelId} was deleted; reposting.")]
117-
private static partial void
118-
LogMessageMissing(ILogger logger, Exception exception, ulong channelId, ulong messageId);
119-
12058
[LoggerMessage(Level = LogLevel.Warning,
12159
Message = "Sending @everyone ping in channel {ChannelId} failed.")]
12260
private static partial void LogPingFailed(ILogger logger, Exception exception, ulong channelId);
Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,19 @@
11
using Discord;
22
using Discord.WebSocket;
33
using Microsoft.Extensions.Logging;
4+
using RustPlusBot.Discord.Posting;
45

56
namespace RustPlusBot.Features.Events.Posting;
67

78
/// <summary>Posts event embeds to Discord text channels via the gateway client.</summary>
89
/// <param name="client">The Discord socket client.</param>
910
/// <param name="logger">The logger.</param>
10-
internal sealed partial class DiscordEventChannelPoster(
11+
internal sealed class DiscordEventChannelPoster(
1112
DiscordSocketClient client,
1213
ILogger<DiscordEventChannelPoster> logger)
1314
: IEventChannelPoster
1415
{
1516
/// <inheritdoc />
16-
public async Task PostAsync(ulong channelId, Embed embed, CancellationToken cancellationToken)
17-
{
18-
try
19-
{
20-
var options = new RequestOptions
21-
{
22-
CancelToken = cancellationToken
23-
};
24-
if (await client.GetChannelAsync(channelId, options).ConfigureAwait(false) is not ITextChannel channel)
25-
{
26-
return;
27-
}
28-
29-
await channel.SendMessageAsync(embed: embed, options: options).ConfigureAwait(false);
30-
}
31-
catch (OperationCanceledException)
32-
{
33-
throw; // Cancellation (shutdown) is not a post failure; let the relay loop unwind cleanly.
34-
}
35-
#pragma warning disable CA1031 // Broad catch: a Discord hiccup must not crash the relay.
36-
catch (Exception ex)
37-
#pragma warning restore CA1031
38-
{
39-
LogPostFailed(logger, ex, channelId);
40-
}
41-
}
42-
43-
[LoggerMessage(Level = LogLevel.Warning, Message = "Posting an event embed to channel {ChannelId} failed.")]
44-
private static partial void LogPostFailed(ILogger logger, Exception exception, ulong channelId);
17+
public Task PostAsync(ulong channelId, Embed embed, CancellationToken cancellationToken)
18+
=> DiscordChannelMessenger.PostAsync(client, channelId, embed, logger, cancellationToken);
4519
}
Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,19 @@
11
using Discord;
22
using Discord.WebSocket;
33
using Microsoft.Extensions.Logging;
4+
using RustPlusBot.Discord.Posting;
45

56
namespace RustPlusBot.Features.Players.Posting;
67

78
/// <summary>Posts player-event embeds to Discord text channels via the gateway client.</summary>
89
/// <param name="client">The Discord socket client.</param>
910
/// <param name="logger">The logger.</param>
10-
internal sealed partial class DiscordPlayerChannelPoster(
11+
internal sealed class DiscordPlayerChannelPoster(
1112
DiscordSocketClient client,
1213
ILogger<DiscordPlayerChannelPoster> logger)
1314
: IPlayerChannelPoster
1415
{
1516
/// <inheritdoc />
16-
public async Task PostAsync(ulong channelId, Embed embed, CancellationToken cancellationToken)
17-
{
18-
try
19-
{
20-
var options = new RequestOptions
21-
{
22-
CancelToken = cancellationToken
23-
};
24-
if (await client.GetChannelAsync(channelId, options).ConfigureAwait(false) is not ITextChannel channel)
25-
{
26-
return;
27-
}
28-
29-
await channel.SendMessageAsync(embed: embed, options: options).ConfigureAwait(false);
30-
}
31-
catch (OperationCanceledException)
32-
{
33-
throw; // Cancellation (shutdown) is not a post failure; let the relay loop unwind cleanly.
34-
}
35-
#pragma warning disable CA1031 // Broad catch: a Discord hiccup must not crash the relay.
36-
catch (Exception ex)
37-
#pragma warning restore CA1031
38-
{
39-
LogPostFailed(logger, ex, channelId);
40-
}
41-
}
42-
43-
[LoggerMessage(Level = LogLevel.Warning, Message = "Posting a player-event embed to channel {ChannelId} failed.")]
44-
private static partial void LogPostFailed(ILogger logger, Exception exception, ulong channelId);
17+
public Task PostAsync(ulong channelId, Embed embed, CancellationToken cancellationToken)
18+
=> DiscordChannelMessenger.PostAsync(client, channelId, embed, logger, cancellationToken);
4519
}

0 commit comments

Comments
 (0)