mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-05-13 17:26:08 +03:00
fix: handle guild data load errors better
This commit is contained in:
parent
02707312f5
commit
67a289a2d6
8 changed files with 140 additions and 27 deletions
|
@ -570,4 +570,10 @@
|
|||
<data name="MessagesClearedFiltered" xml:space="preserve">
|
||||
<value>Cleared {0} messages from {1}</value>
|
||||
</data>
|
||||
<data name="DataLoadFailedTitle" xml:space="preserve">
|
||||
<value>An error occurred during guild data load.</value>
|
||||
</data>
|
||||
<data name="DataLoadFailedDescription" xml:space="preserve">
|
||||
<value>This will lead to unexpected behavior. Data will no longer be saved</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
@ -570,4 +570,10 @@
|
|||
<data name="MessagesClearedFiltered" xml:space="preserve">
|
||||
<value>Очищено {0} сообщений от {1}</value>
|
||||
</data>
|
||||
<data name="DataLoadFailedTitle" xml:space="preserve">
|
||||
<value>Произошла ошибка при загрузке данных сервера.</value>
|
||||
</data>
|
||||
<data name="DataLoadFailedDescription" xml:space="preserve">
|
||||
<value>Это может привести к неожиданному поведению. Данные больше не будут сохраняться.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
@ -570,4 +570,10 @@
|
|||
<data name="MessagesClearedFiltered" xml:space="preserve">
|
||||
<value>вырезано {0} забавных сообщений от {1}</value>
|
||||
</data>
|
||||
<data name="DataLoadFailedTitle" xml:space="preserve">
|
||||
<value>произошёл пиздец в гилддате.</value>
|
||||
</data>
|
||||
<data name="DataLoadFailedDescription" xml:space="preserve">
|
||||
<value>возможно всё съедет с крыши, но знай, что я больше ничё не сохраню.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
@ -17,10 +17,12 @@ public sealed class GuildData
|
|||
public readonly JsonNode Settings;
|
||||
public readonly string SettingsPath;
|
||||
|
||||
public readonly bool DataLoadFailed;
|
||||
|
||||
public GuildData(
|
||||
JsonNode settings, string settingsPath,
|
||||
Dictionary<ulong, ScheduledEventData> scheduledEvents, string scheduledEventsPath,
|
||||
Dictionary<ulong, MemberData> memberData, string memberDataPath)
|
||||
Dictionary<ulong, MemberData> memberData, string memberDataPath, bool dataLoadFailed)
|
||||
{
|
||||
Settings = settings;
|
||||
SettingsPath = settingsPath;
|
||||
|
@ -28,6 +30,7 @@ public sealed class GuildData
|
|||
ScheduledEventsPath = scheduledEventsPath;
|
||||
MemberData = memberData;
|
||||
MemberDataPath = memberDataPath;
|
||||
DataLoadFailed = dataLoadFailed;
|
||||
}
|
||||
|
||||
public MemberData GetOrCreateMemberData(Snowflake memberId)
|
||||
|
|
16
src/Messages.Designer.cs
generated
16
src/Messages.Designer.cs
generated
|
@ -996,5 +996,21 @@ namespace Octobot {
|
|||
return ResourceManager.GetString("MessagesClearedFiltered", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string DataLoadFailedTitle
|
||||
{
|
||||
get
|
||||
{
|
||||
return ResourceManager.GetString("DataLoadFailedTitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string DataLoadFailedDescription
|
||||
{
|
||||
get
|
||||
{
|
||||
return ResourceManager.GetString("DataLoadFailedDescription", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ using Remora.Discord.API.Abstractions.Rest;
|
|||
using Remora.Discord.API.Gateway.Events;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Discord.Gateway.Responders;
|
||||
using Remora.Rest.Core;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Octobot.Responders;
|
||||
|
@ -42,7 +43,6 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
|
|||
}
|
||||
|
||||
var guild = gatewayEvent.Guild.AsT0;
|
||||
_logger.LogInformation("Joined guild {ID} (\"{Name}\")", guild.ID, guild.Name);
|
||||
|
||||
var data = await _guildData.GetData(guild.ID, ct);
|
||||
var cfg = data.Settings;
|
||||
|
@ -51,6 +51,31 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
|
|||
data.GetOrCreateMemberData(member.User.Value.ID);
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(ct);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
}
|
||||
|
||||
if (data.DataLoadFailed)
|
||||
{
|
||||
var errorEmbed = new EmbedBuilder()
|
||||
.WithSmallTitle(Messages.DataLoadFailedTitle, bot)
|
||||
.WithDescription(Messages.DataLoadFailedDescription)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
if (!errorEmbed.IsDefined(out var errorBuilt))
|
||||
{
|
||||
return Result.FromError(errorEmbed);
|
||||
}
|
||||
|
||||
return (Result)await _channelApi.CreateMessageAsync(
|
||||
GetEmergencyFeedbackChannel(guild, data), embeds: new[] { errorBuilt }, ct: ct);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Loaded guild {ID} (\"{Name}\")", guild.ID, guild.Name);
|
||||
|
||||
if (!GuildSettings.ReceiveStartupMessages.Get(cfg))
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
|
@ -61,12 +86,6 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
|
|||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(ct);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
}
|
||||
|
||||
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||
var i = Random.Shared.Next(1, 4);
|
||||
|
||||
|
@ -84,4 +103,23 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
|
|||
return (Result)await _channelApi.CreateMessageAsync(
|
||||
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, ct: ct);
|
||||
}
|
||||
|
||||
private static Snowflake GetEmergencyFeedbackChannel(IGuildCreate.IAvailableGuild guild, GuildData data)
|
||||
{
|
||||
var privateFeedback = GuildSettings.PrivateFeedbackChannel.Get(data.Settings);
|
||||
if (!privateFeedback.Empty())
|
||||
{
|
||||
return privateFeedback;
|
||||
}
|
||||
|
||||
var publicFeedback = GuildSettings.PublicFeedbackChannel.Get(data.Settings);
|
||||
if (!publicFeedback.Empty())
|
||||
{
|
||||
return publicFeedback;
|
||||
}
|
||||
|
||||
return guild.SystemChannelID.AsOptional().IsDefined(out var systemChannel)
|
||||
? systemChannel
|
||||
: guild.Channels[0].ID;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ public class GuildUnloadedResponder : IResponder<IGuildDelete>
|
|||
var isDataRemoved = _guildData.UnloadGuildData(guildId);
|
||||
if (isDataRemoved)
|
||||
{
|
||||
_logger.LogInformation("Left guild {GuildId}", guildId);
|
||||
_logger.LogInformation("Unloaded guild {GuildId}", guildId);
|
||||
}
|
||||
|
||||
return Task.FromResult(Result.FromSuccess());
|
||||
|
|
|
@ -43,25 +43,31 @@ public sealed class GuildDataService : IHostedService
|
|||
{
|
||||
var tasks = new List<Task>();
|
||||
var datas = _datas.Values.ToArray();
|
||||
foreach (var data in datas)
|
||||
foreach (var data in datas.Where(data => !data.DataLoadFailed))
|
||||
{
|
||||
await using var settingsStream = File.Create(data.SettingsPath);
|
||||
tasks.Add(JsonSerializer.SerializeAsync(settingsStream, data.Settings, cancellationToken: ct));
|
||||
|
||||
await using var eventsStream = File.Create(data.ScheduledEventsPath);
|
||||
tasks.Add(JsonSerializer.SerializeAsync(eventsStream, data.ScheduledEvents, cancellationToken: ct));
|
||||
tasks.Add(SerializeObjectSafelyAsync(data.Settings, data.SettingsPath, ct));
|
||||
tasks.Add(SerializeObjectSafelyAsync(data.ScheduledEvents, data.ScheduledEventsPath, ct));
|
||||
|
||||
var memberDatas = data.MemberData.Values.ToArray();
|
||||
foreach (var memberData in memberDatas)
|
||||
{
|
||||
await using var memberDataStream = File.Create($"{data.MemberDataPath}/{memberData.Id}.json");
|
||||
tasks.Add(JsonSerializer.SerializeAsync(memberDataStream, memberData, cancellationToken: ct));
|
||||
}
|
||||
tasks.AddRange(memberDatas.Select(memberData =>
|
||||
SerializeObjectSafelyAsync(memberData, $"{data.MemberDataPath}/{memberData.Id}.json", ct)));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private static async Task SerializeObjectSafelyAsync<T>(T obj, string path, CancellationToken ct)
|
||||
{
|
||||
var tempFilePath = path + ".tmp";
|
||||
await using (var tempFileStream = File.Create(tempFilePath))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(tempFileStream, obj, cancellationToken: ct);
|
||||
}
|
||||
|
||||
File.Copy(tempFilePath, path, true);
|
||||
File.Delete(tempFilePath);
|
||||
}
|
||||
|
||||
public async Task<GuildData> GetData(Snowflake guildId, CancellationToken ct = default)
|
||||
{
|
||||
return _datas.TryGetValue(guildId, out var data) ? data : await InitializeData(guildId, ct);
|
||||
|
@ -88,20 +94,50 @@ public sealed class GuildDataService : IHostedService
|
|||
await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct);
|
||||
}
|
||||
|
||||
var dataLoadFailed = false;
|
||||
|
||||
await using var settingsStream = File.OpenRead(settingsPath);
|
||||
var jsonSettings
|
||||
= JsonNode.Parse(settingsStream);
|
||||
JsonNode? jsonSettings = null;
|
||||
try
|
||||
{
|
||||
jsonSettings = JsonNode.Parse(settingsStream);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Guild settings load failed: {Path}", settingsPath);
|
||||
dataLoadFailed = true;
|
||||
}
|
||||
|
||||
await using var eventsStream = File.OpenRead(scheduledEventsPath);
|
||||
var events
|
||||
= await JsonSerializer.DeserializeAsync<Dictionary<ulong, ScheduledEventData>>(
|
||||
Dictionary<ulong, ScheduledEventData>? events = null;
|
||||
try
|
||||
{
|
||||
events = await JsonSerializer.DeserializeAsync<Dictionary<ulong, ScheduledEventData>>(
|
||||
eventsStream, cancellationToken: ct);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Guild scheduled events load failed: {Path}", scheduledEventsPath);
|
||||
dataLoadFailed = true;
|
||||
}
|
||||
|
||||
var memberData = new Dictionary<ulong, MemberData>();
|
||||
foreach (var dataFileInfo in Directory.CreateDirectory(memberDataPath).GetFiles())
|
||||
{
|
||||
await using var dataStream = dataFileInfo.OpenRead();
|
||||
var data = await JsonSerializer.DeserializeAsync<MemberData>(dataStream, cancellationToken: ct);
|
||||
MemberData? data;
|
||||
try
|
||||
{
|
||||
data = await JsonSerializer.DeserializeAsync<MemberData>(dataStream, cancellationToken: ct);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Member data load failed: {MemberDataPath}/{FileName}", memberDataPath,
|
||||
dataFileInfo.Name);
|
||||
dataLoadFailed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data is null)
|
||||
{
|
||||
continue;
|
||||
|
@ -113,7 +149,8 @@ public sealed class GuildDataService : IHostedService
|
|||
var finalData = new GuildData(
|
||||
jsonSettings ?? new JsonObject(), settingsPath,
|
||||
events ?? new Dictionary<ulong, ScheduledEventData>(), scheduledEventsPath,
|
||||
memberData, memberDataPath);
|
||||
memberData, memberDataPath,
|
||||
dataLoadFailed);
|
||||
|
||||
_datas.TryAdd(guildId, finalData);
|
||||
|
||||
|
@ -129,7 +166,8 @@ public sealed class GuildDataService : IHostedService
|
|||
Directory.CreateDirectory($"{newPath}/..");
|
||||
Directory.Move(oldPath, newPath);
|
||||
|
||||
_logger.LogInformation("Moved guild data to separate folder: \"{OldPath}\" -> \"{NewPath}\"", oldPath, newPath);
|
||||
_logger.LogInformation("Moved guild data to separate folder: \"{OldPath}\" -> \"{NewPath}\"", oldPath,
|
||||
newPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue