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">
|
<data name="MessagesClearedFiltered" xml:space="preserve">
|
||||||
<value>Cleared {0} messages from {1}</value>
|
<value>Cleared {0} messages from {1}</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
|
|
|
@ -570,4 +570,10 @@
|
||||||
<data name="MessagesClearedFiltered" xml:space="preserve">
|
<data name="MessagesClearedFiltered" xml:space="preserve">
|
||||||
<value>Очищено {0} сообщений от {1}</value>
|
<value>Очищено {0} сообщений от {1}</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DataLoadFailedTitle" xml:space="preserve">
|
||||||
|
<value>Произошла ошибка при загрузке данных сервера.</value>
|
||||||
|
</data>
|
||||||
|
<data name="DataLoadFailedDescription" xml:space="preserve">
|
||||||
|
<value>Это может привести к неожиданному поведению. Данные больше не будут сохраняться.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
@ -570,4 +570,10 @@
|
||||||
<data name="MessagesClearedFiltered" xml:space="preserve">
|
<data name="MessagesClearedFiltered" xml:space="preserve">
|
||||||
<value>вырезано {0} забавных сообщений от {1}</value>
|
<value>вырезано {0} забавных сообщений от {1}</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DataLoadFailedTitle" xml:space="preserve">
|
||||||
|
<value>произошёл пиздец в гилддате.</value>
|
||||||
|
</data>
|
||||||
|
<data name="DataLoadFailedDescription" xml:space="preserve">
|
||||||
|
<value>возможно всё съедет с крыши, но знай, что я больше ничё не сохраню.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
@ -17,10 +17,12 @@ public sealed class GuildData
|
||||||
public readonly JsonNode Settings;
|
public readonly JsonNode Settings;
|
||||||
public readonly string SettingsPath;
|
public readonly string SettingsPath;
|
||||||
|
|
||||||
|
public readonly bool DataLoadFailed;
|
||||||
|
|
||||||
public GuildData(
|
public GuildData(
|
||||||
JsonNode settings, string settingsPath,
|
JsonNode settings, string settingsPath,
|
||||||
Dictionary<ulong, ScheduledEventData> scheduledEvents, string scheduledEventsPath,
|
Dictionary<ulong, ScheduledEventData> scheduledEvents, string scheduledEventsPath,
|
||||||
Dictionary<ulong, MemberData> memberData, string memberDataPath)
|
Dictionary<ulong, MemberData> memberData, string memberDataPath, bool dataLoadFailed)
|
||||||
{
|
{
|
||||||
Settings = settings;
|
Settings = settings;
|
||||||
SettingsPath = settingsPath;
|
SettingsPath = settingsPath;
|
||||||
|
@ -28,6 +30,7 @@ public sealed class GuildData
|
||||||
ScheduledEventsPath = scheduledEventsPath;
|
ScheduledEventsPath = scheduledEventsPath;
|
||||||
MemberData = memberData;
|
MemberData = memberData;
|
||||||
MemberDataPath = memberDataPath;
|
MemberDataPath = memberDataPath;
|
||||||
|
DataLoadFailed = dataLoadFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MemberData GetOrCreateMemberData(Snowflake memberId)
|
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);
|
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.API.Gateway.Events;
|
||||||
using Remora.Discord.Extensions.Embeds;
|
using Remora.Discord.Extensions.Embeds;
|
||||||
using Remora.Discord.Gateway.Responders;
|
using Remora.Discord.Gateway.Responders;
|
||||||
|
using Remora.Rest.Core;
|
||||||
using Remora.Results;
|
using Remora.Results;
|
||||||
|
|
||||||
namespace Octobot.Responders;
|
namespace Octobot.Responders;
|
||||||
|
@ -42,7 +43,6 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
|
||||||
}
|
}
|
||||||
|
|
||||||
var guild = gatewayEvent.Guild.AsT0;
|
var guild = gatewayEvent.Guild.AsT0;
|
||||||
_logger.LogInformation("Joined guild {ID} (\"{Name}\")", guild.ID, guild.Name);
|
|
||||||
|
|
||||||
var data = await _guildData.GetData(guild.ID, ct);
|
var data = await _guildData.GetData(guild.ID, ct);
|
||||||
var cfg = data.Settings;
|
var cfg = data.Settings;
|
||||||
|
@ -51,6 +51,31 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
|
||||||
data.GetOrCreateMemberData(member.User.Value.ID);
|
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))
|
if (!GuildSettings.ReceiveStartupMessages.Get(cfg))
|
||||||
{
|
{
|
||||||
return Result.FromSuccess();
|
return Result.FromSuccess();
|
||||||
|
@ -61,12 +86,6 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
|
||||||
return Result.FromSuccess();
|
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);
|
Messages.Culture = GuildSettings.Language.Get(cfg);
|
||||||
var i = Random.Shared.Next(1, 4);
|
var i = Random.Shared.Next(1, 4);
|
||||||
|
|
||||||
|
@ -84,4 +103,23 @@ public class GuildLoadedResponder : IResponder<IGuildCreate>
|
||||||
return (Result)await _channelApi.CreateMessageAsync(
|
return (Result)await _channelApi.CreateMessageAsync(
|
||||||
GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, ct: ct);
|
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);
|
var isDataRemoved = _guildData.UnloadGuildData(guildId);
|
||||||
if (isDataRemoved)
|
if (isDataRemoved)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Left guild {GuildId}", guildId);
|
_logger.LogInformation("Unloaded guild {GuildId}", guildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(Result.FromSuccess());
|
return Task.FromResult(Result.FromSuccess());
|
||||||
|
|
|
@ -43,25 +43,31 @@ public sealed class GuildDataService : IHostedService
|
||||||
{
|
{
|
||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
var datas = _datas.Values.ToArray();
|
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(SerializeObjectSafelyAsync(data.Settings, data.SettingsPath, ct));
|
||||||
tasks.Add(JsonSerializer.SerializeAsync(settingsStream, data.Settings, cancellationToken: ct));
|
tasks.Add(SerializeObjectSafelyAsync(data.ScheduledEvents, data.ScheduledEventsPath, ct));
|
||||||
|
|
||||||
await using var eventsStream = File.Create(data.ScheduledEventsPath);
|
|
||||||
tasks.Add(JsonSerializer.SerializeAsync(eventsStream, data.ScheduledEvents, cancellationToken: ct));
|
|
||||||
|
|
||||||
var memberDatas = data.MemberData.Values.ToArray();
|
var memberDatas = data.MemberData.Values.ToArray();
|
||||||
foreach (var memberData in memberDatas)
|
tasks.AddRange(memberDatas.Select(memberData =>
|
||||||
{
|
SerializeObjectSafelyAsync(memberData, $"{data.MemberDataPath}/{memberData.Id}.json", ct)));
|
||||||
await using var memberDataStream = File.Create($"{data.MemberDataPath}/{memberData.Id}.json");
|
|
||||||
tasks.Add(JsonSerializer.SerializeAsync(memberDataStream, memberData, cancellationToken: ct));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
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)
|
public async Task<GuildData> GetData(Snowflake guildId, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return _datas.TryGetValue(guildId, out var data) ? data : await InitializeData(guildId, ct);
|
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);
|
await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var dataLoadFailed = false;
|
||||||
|
|
||||||
await using var settingsStream = File.OpenRead(settingsPath);
|
await using var settingsStream = File.OpenRead(settingsPath);
|
||||||
var jsonSettings
|
JsonNode? jsonSettings = null;
|
||||||
= JsonNode.Parse(settingsStream);
|
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);
|
await using var eventsStream = File.OpenRead(scheduledEventsPath);
|
||||||
var events
|
Dictionary<ulong, ScheduledEventData>? events = null;
|
||||||
= await JsonSerializer.DeserializeAsync<Dictionary<ulong, ScheduledEventData>>(
|
try
|
||||||
|
{
|
||||||
|
events = await JsonSerializer.DeserializeAsync<Dictionary<ulong, ScheduledEventData>>(
|
||||||
eventsStream, cancellationToken: ct);
|
eventsStream, cancellationToken: ct);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Guild scheduled events load failed: {Path}", scheduledEventsPath);
|
||||||
|
dataLoadFailed = true;
|
||||||
|
}
|
||||||
|
|
||||||
var memberData = new Dictionary<ulong, MemberData>();
|
var memberData = new Dictionary<ulong, MemberData>();
|
||||||
foreach (var dataFileInfo in Directory.CreateDirectory(memberDataPath).GetFiles())
|
foreach (var dataFileInfo in Directory.CreateDirectory(memberDataPath).GetFiles())
|
||||||
{
|
{
|
||||||
await using var dataStream = dataFileInfo.OpenRead();
|
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)
|
if (data is null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
@ -113,7 +149,8 @@ public sealed class GuildDataService : IHostedService
|
||||||
var finalData = new GuildData(
|
var finalData = new GuildData(
|
||||||
jsonSettings ?? new JsonObject(), settingsPath,
|
jsonSettings ?? new JsonObject(), settingsPath,
|
||||||
events ?? new Dictionary<ulong, ScheduledEventData>(), scheduledEventsPath,
|
events ?? new Dictionary<ulong, ScheduledEventData>(), scheduledEventsPath,
|
||||||
memberData, memberDataPath);
|
memberData, memberDataPath,
|
||||||
|
dataLoadFailed);
|
||||||
|
|
||||||
_datas.TryAdd(guildId, finalData);
|
_datas.TryAdd(guildId, finalData);
|
||||||
|
|
||||||
|
@ -129,7 +166,8 @@ public sealed class GuildDataService : IHostedService
|
||||||
Directory.CreateDirectory($"{newPath}/..");
|
Directory.CreateDirectory($"{newPath}/..");
|
||||||
Directory.Move(oldPath, 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