Skip to content

Commit deb0c7f

Browse files
committed
feat(switches): add DiscordSwitchChannelPoster (edit-by-id with self-heal)
1 parent 4decb7b commit deb0c7f

2 files changed

Lines changed: 101 additions & 0 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using Discord.Net;
2+
using Discord.WebSocket;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace RustPlusBot.Features.Switches.Posting;
6+
7+
/// <summary>Posts/edits switch embeds in #switches by message id. Untested integration shim.</summary>
8+
/// <param name="client">The Discord socket client.</param>
9+
/// <param name="logger">The logger.</param>
10+
internal sealed partial class DiscordSwitchChannelPoster(
11+
DiscordSocketClient client,
12+
ILogger<DiscordSwitchChannelPoster> logger) : ISwitchChannelPoster
13+
{
14+
/// <inheritdoc />
15+
public async Task<ulong?> EnsureAsync(
16+
ulong channelId,
17+
ulong? messageId,
18+
global::Discord.Embed embed,
19+
global::Discord.MessageComponent components,
20+
CancellationToken cancellationToken)
21+
{
22+
try
23+
{
24+
var options = new global::Discord.RequestOptions { CancelToken = cancellationToken };
25+
if (await client.GetChannelAsync(channelId, options).ConfigureAwait(false)
26+
is not global::Discord.ITextChannel channel)
27+
{
28+
return null;
29+
}
30+
31+
if (messageId is { } id)
32+
{
33+
// Inner try: some Discord.Net versions THROW (HttpException 404/Unknown Message)
34+
// rather than return null for a deleted message. Catch it and fall through to repost
35+
// so the self-heal path always runs.
36+
try
37+
{
38+
var existing = await channel.GetMessageAsync(id, options: options).ConfigureAwait(false);
39+
if (existing is global::Discord.IUserMessage userMessage)
40+
{
41+
await userMessage.ModifyAsync(m =>
42+
{
43+
m.Embed = embed;
44+
m.Components = components;
45+
}, options).ConfigureAwait(false);
46+
return userMessage.Id;
47+
}
48+
49+
// Message was deleted (returned null / not a user message); fall through to repost.
50+
}
51+
catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound)
52+
{
53+
// Deleted/unknown message; fall through to repost and return the new id.
54+
LogMessageMissing(logger, ex, channelId, id);
55+
}
56+
}
57+
58+
var posted = await channel
59+
.SendMessageAsync(embed: embed, options: options, components: components)
60+
.ConfigureAwait(false);
61+
return posted.Id;
62+
}
63+
catch (OperationCanceledException)
64+
{
65+
throw; // Shutdown — let the loop unwind.
66+
}
67+
#pragma warning disable CA1031 // Broad catch: a Discord hiccup must not crash the relay; report failure as null.
68+
catch (Exception ex)
69+
#pragma warning restore CA1031
70+
{
71+
LogEnsureFailed(logger, ex, channelId);
72+
return null;
73+
}
74+
}
75+
76+
[LoggerMessage(Level = LogLevel.Warning, Message = "Posting/editing a switch embed in channel {ChannelId} failed.")]
77+
private static partial void LogEnsureFailed(ILogger logger, Exception exception, ulong channelId);
78+
79+
[LoggerMessage(Level = LogLevel.Debug,
80+
Message = "Switch embed {MessageId} in channel {ChannelId} was deleted; reposting.")]
81+
private static partial void LogMessageMissing(ILogger logger, Exception exception, ulong channelId, ulong messageId);
82+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace RustPlusBot.Features.Switches.Posting;
2+
3+
/// <summary>Posts/edits a switch embed in #switches by message id, self-healing a deleted message.</summary>
4+
internal interface ISwitchChannelPoster
5+
{
6+
/// <summary>Edits the message at <paramref name="messageId"/> if present and found; otherwise posts a new one.</summary>
7+
/// <param name="channelId">The #switches channel id.</param>
8+
/// <param name="messageId">The known embed message id, or null to post fresh.</param>
9+
/// <param name="embed">The embed to show.</param>
10+
/// <param name="components">The control row.</param>
11+
/// <param name="cancellationToken">A cancellation token.</param>
12+
/// <returns>The (possibly new) message id, or null on failure.</returns>
13+
Task<ulong?> EnsureAsync(
14+
ulong channelId,
15+
ulong? messageId,
16+
global::Discord.Embed embed,
17+
global::Discord.MessageComponent components,
18+
CancellationToken cancellationToken);
19+
}

0 commit comments

Comments
 (0)