mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-04-19 16:33:36 +03:00
Add support for temporary bans
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
This commit is contained in:
parent
e883e143eb
commit
9c080d9691
10 changed files with 144 additions and 31 deletions
|
@ -74,6 +74,7 @@ public class Boyfriend {
|
|||
.AddInteractionGroup<InteractionResponders>()
|
||||
.AddSingleton<GuildDataService>()
|
||||
.AddSingleton<UtilityService>()
|
||||
.AddHostedService<GuildUpdateService>()
|
||||
.AddCommandTree()
|
||||
.WithCommandGroup<BanCommandGroup>();
|
||||
var responderTypes = typeof(Boyfriend).Assembly
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System.ComponentModel;
|
||||
using System.Net;
|
||||
using Boyfriend.Services;
|
||||
using Boyfriend.Services.Data;
|
||||
using Remora.Commands.Attributes;
|
||||
|
@ -48,8 +47,9 @@ public class BanCommandGroup : CommandGroup {
|
|||
/// A slash command that bans a Discord user with the specified reason.
|
||||
/// </summary>
|
||||
/// <param name="target">The user to ban.</param>
|
||||
/// <param name="duration">The duration for this ban. The user will be automatically unbanned after this duration.</param>
|
||||
/// <param name="reason">
|
||||
/// The reason for this ban. Must be encoded with <see cref="WebUtility.UrlEncode" /> when passed to
|
||||
/// The reason for this ban. Must be encoded with <see cref="Extensions.EncodeHeader" /> when passed to
|
||||
/// <see cref="IDiscordRestGuildAPI.CreateGuildBanAsync" />.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
|
@ -62,7 +62,8 @@ public class BanCommandGroup : CommandGroup {
|
|||
[RequireDiscordPermission(DiscordPermission.BanMembers)]
|
||||
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
|
||||
[Description("банит пидора")]
|
||||
public async Task<Result> BanUserAsync([Description("Юзер, кого банить")] IUser target, string reason) {
|
||||
public async Task<Result> BanUserAsync(
|
||||
[Description("Юзер, кого банить")] IUser target, string reason, TimeSpan? duration = null) {
|
||||
// Data checks
|
||||
if (!_context.TryGetGuildID(out var guildId))
|
||||
return Result.FromError(new ArgumentNullError(nameof(guildId)));
|
||||
|
@ -76,8 +77,9 @@ public class BanCommandGroup : CommandGroup {
|
|||
if (!currentUserResult.IsDefined(out var currentUser))
|
||||
return Result.FromError(currentUserResult);
|
||||
|
||||
var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
|
||||
Messages.Culture = cfg.Culture;
|
||||
var data = await _dataService.GetData(guildId.Value, CancellationToken);
|
||||
var cfg = data.Configuration;
|
||||
Messages.Culture = data.Culture;
|
||||
|
||||
var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken);
|
||||
if (existingBanResult.IsDefined()) {
|
||||
|
@ -105,10 +107,12 @@ public class BanCommandGroup : CommandGroup {
|
|||
return Result.FromError(userResult);
|
||||
|
||||
var banResult = await _guildApi.CreateGuildBanAsync(
|
||||
guildId.Value, target.ID, reason: $"({user.GetTag()}) {WebUtility.UrlEncode(reason)}",
|
||||
guildId.Value, target.ID, reason: $"({user.GetTag()}) {reason.EncodeHeader()}",
|
||||
ct: CancellationToken);
|
||||
if (!banResult.IsSuccess)
|
||||
return Result.FromError(banResult.Error);
|
||||
data.GetMemberData(target.ID).BannedUntil
|
||||
= duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue;
|
||||
|
||||
responseEmbed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.UserBanned, target.GetTag()), target)
|
||||
|
@ -151,7 +155,7 @@ public class BanCommandGroup : CommandGroup {
|
|||
/// </summary>
|
||||
/// <param name="target">The user to unban.</param>
|
||||
/// <param name="reason">
|
||||
/// The reason for this unban. Must be encoded with <see cref="WebUtility.UrlEncode" /> when passed to
|
||||
/// The reason for this unban. Must be encoded with <see cref="Extensions.EncodeHeader" /> when passed to
|
||||
/// <see cref="IDiscordRestGuildAPI.RemoveGuildBanAsync" />.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
|
@ -179,7 +183,7 @@ public class BanCommandGroup : CommandGroup {
|
|||
return Result.FromError(currentUserResult);
|
||||
|
||||
var cfg = await _dataService.GetConfiguration(guildId.Value, CancellationToken);
|
||||
Messages.Culture = cfg.Culture;
|
||||
Messages.Culture = cfg.GetCulture();
|
||||
|
||||
var existingBanResult = await _guildApi.GetGuildBanAsync(guildId.Value, target.ID, CancellationToken);
|
||||
if (!existingBanResult.IsDefined()) {
|
||||
|
@ -198,7 +202,7 @@ public class BanCommandGroup : CommandGroup {
|
|||
return Result.FromError(userResult);
|
||||
|
||||
var unbanResult = await _guildApi.RemoveGuildBanAsync(
|
||||
guildId.Value, target.ID, reason: $"({user.GetTag()}) {WebUtility.UrlEncode(reason)}",
|
||||
guildId.Value, target.ID, $"({user.GetTag()}) {reason.EncodeHeader()}",
|
||||
ct: CancellationToken);
|
||||
if (!unbanResult.IsSuccess)
|
||||
return Result.FromError(unbanResult.Error);
|
||||
|
@ -232,7 +236,6 @@ public class BanCommandGroup : CommandGroup {
|
|||
ct: CancellationToken);
|
||||
}
|
||||
|
||||
|
||||
if (!responseEmbed.IsDefined(out var built))
|
||||
return Result.FromError(responseEmbed);
|
||||
|
||||
|
|
|
@ -83,5 +83,7 @@ public class GuildConfiguration {
|
|||
/// </summary>
|
||||
public TimeSpan EventEarlyNotificationOffset { get; set; } = TimeSpan.Zero;
|
||||
|
||||
public CultureInfo Culture => CultureInfoCache[Language];
|
||||
public CultureInfo GetCulture() {
|
||||
return CultureInfoCache[Language];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Globalization;
|
||||
using Remora.Rest.Core;
|
||||
|
||||
namespace Boyfriend.Data;
|
||||
|
||||
|
@ -10,17 +11,31 @@ public class GuildData {
|
|||
public readonly GuildConfiguration Configuration;
|
||||
public readonly string ConfigurationPath;
|
||||
|
||||
public readonly Dictionary<ulong, MemberData> MemberData;
|
||||
public readonly string MemberDataPath;
|
||||
|
||||
public readonly Dictionary<ulong, ScheduledEventData> ScheduledEvents;
|
||||
public readonly string ScheduledEventsPath;
|
||||
|
||||
public GuildData(
|
||||
GuildConfiguration configuration, string configurationPath,
|
||||
Dictionary<ulong, ScheduledEventData> scheduledEvents, string scheduledEventsPath) {
|
||||
Dictionary<ulong, ScheduledEventData> scheduledEvents, string scheduledEventsPath,
|
||||
Dictionary<ulong, MemberData> memberData, string memberDataPath) {
|
||||
Configuration = configuration;
|
||||
ConfigurationPath = configurationPath;
|
||||
ScheduledEvents = scheduledEvents;
|
||||
ScheduledEventsPath = scheduledEventsPath;
|
||||
MemberData = memberData;
|
||||
MemberDataPath = memberDataPath;
|
||||
}
|
||||
|
||||
public CultureInfo Culture => Configuration.Culture;
|
||||
public CultureInfo Culture => Configuration.GetCulture();
|
||||
|
||||
public MemberData GetMemberData(Snowflake userId) {
|
||||
if (MemberData.TryGetValue(userId.Value, out var existing)) return existing;
|
||||
|
||||
var newData = new MemberData(userId.Value, null);
|
||||
MemberData.Add(userId.Value, newData);
|
||||
return newData;
|
||||
}
|
||||
}
|
||||
|
|
11
Data/MemberData.cs
Normal file
11
Data/MemberData.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
namespace Boyfriend.Data;
|
||||
|
||||
public class MemberData {
|
||||
public MemberData(ulong id, DateTimeOffset? bannedUntil) {
|
||||
Id = id;
|
||||
BannedUntil = bannedUntil;
|
||||
}
|
||||
|
||||
public ulong Id { get; }
|
||||
public DateTimeOffset? BannedUntil { get; set; }
|
||||
}
|
|
@ -7,10 +7,10 @@ namespace Boyfriend.Data;
|
|||
/// </summary>
|
||||
/// <remarks>This information is stored on disk as a JSON file.</remarks>
|
||||
public class ScheduledEventData {
|
||||
public DateTimeOffset? ActualStartTime;
|
||||
public GuildScheduledEventStatus Status;
|
||||
|
||||
public ScheduledEventData(GuildScheduledEventStatus status) {
|
||||
Status = status;
|
||||
}
|
||||
|
||||
public DateTimeOffset? ActualStartTime { get; set; }
|
||||
public GuildScheduledEventStatus Status { get; set; }
|
||||
}
|
||||
|
|
|
@ -32,12 +32,12 @@ public class GuildCreateResponder : IResponder<IGuildCreate> {
|
|||
private readonly IDiscordRestUserAPI _userApi;
|
||||
|
||||
public GuildCreateResponder(
|
||||
IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestUserAPI userApi,
|
||||
ILogger<GuildCreateResponder> logger) {
|
||||
IDiscordRestChannelAPI channelApi, GuildDataService dataService, ILogger<GuildCreateResponder> logger,
|
||||
IDiscordRestUserAPI userApi) {
|
||||
_channelApi = channelApi;
|
||||
_dataService = dataService;
|
||||
_userApi = userApi;
|
||||
_logger = logger;
|
||||
_userApi = userApi;
|
||||
}
|
||||
|
||||
public async Task<Result> RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) {
|
||||
|
@ -55,7 +55,7 @@ public class GuildCreateResponder : IResponder<IGuildCreate> {
|
|||
var currentUserResult = await _userApi.GetCurrentUserAsync(ct);
|
||||
if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult);
|
||||
|
||||
Messages.Culture = guildConfig.Culture;
|
||||
Messages.Culture = guildConfig.GetCulture();
|
||||
var i = Random.Shared.Next(1, 4);
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
|
@ -116,7 +116,7 @@ public class MessageDeletedResponder : IResponder<IMessageDelete> {
|
|||
if (!userResult.IsDefined(out user)) return Result.FromError(userResult);
|
||||
}
|
||||
|
||||
Messages.Culture = guildConfiguration.Culture;
|
||||
Messages.Culture = guildConfiguration.GetCulture();
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithSmallTitle(
|
||||
|
@ -194,7 +194,7 @@ public class MessageEditedResponder : IResponder<IMessageUpdate> {
|
|||
|
||||
var diff = new SideBySideDiffBuilder(Differ.Instance).BuildDiffModel(message.Content, newContent, true, true);
|
||||
|
||||
Messages.Culture = guildConfiguration.Culture;
|
||||
Messages.Culture = guildConfiguration.GetCulture();
|
||||
|
||||
var embed = new EmbedBuilder()
|
||||
.WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author)
|
||||
|
@ -234,7 +234,7 @@ public class GuildMemberAddResponder : IResponder<IGuildMemberAdd> {
|
|||
if (guildConfiguration.WelcomeMessage is "off" or "disable" or "disabled")
|
||||
return Result.FromSuccess();
|
||||
|
||||
Messages.Culture = guildConfiguration.Culture;
|
||||
Messages.Culture = guildConfiguration.GetCulture();
|
||||
var welcomeMessage = guildConfiguration.WelcomeMessage is "default" or "reset"
|
||||
? Messages.DefaultWelcomeMessage
|
||||
: guildConfiguration.WelcomeMessage;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
using System.Net;
|
||||
using System.Text;
|
||||
using DiffPlex.DiffBuilder.Model;
|
||||
using Remora.Discord.API;
|
||||
|
@ -108,6 +109,10 @@ public static class Extensions {
|
|||
return Messages.ResourceManager.GetString(key, Messages.Culture) ?? key;
|
||||
}
|
||||
|
||||
public static string EncodeHeader(this string s) {
|
||||
return WebUtility.UrlEncode(s).Replace('+', ' ');
|
||||
}
|
||||
|
||||
public static string AsMarkdown(this SideBySideDiffModel model) {
|
||||
var builder = new StringBuilder();
|
||||
foreach (var line in model.OldText.Lines.Where(piece => !string.IsNullOrWhiteSpace(piece.Text)))
|
||||
|
|
|
@ -11,11 +11,24 @@ namespace Boyfriend.Services.Data;
|
|||
public class GuildDataService : IHostedService {
|
||||
private readonly Dictionary<Snowflake, GuildData> _datas = new();
|
||||
|
||||
// https://github.com/dotnet/aspnetcore/issues/39139
|
||||
public GuildDataService(IHostApplicationLifetime lifetime) {
|
||||
lifetime.ApplicationStopping.Register(ApplicationStopping);
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken ct) {
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken ct) {
|
||||
public Task StopAsync(CancellationToken ct) {
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ApplicationStopping() {
|
||||
SaveAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private async Task SaveAsync(CancellationToken ct) {
|
||||
var tasks = new List<Task>();
|
||||
foreach (var data in _datas.Values) {
|
||||
await using var configStream = File.OpenWrite(data.ConfigurationPath);
|
||||
|
@ -23,6 +36,11 @@ public class GuildDataService : IHostedService {
|
|||
|
||||
await using var eventsStream = File.OpenWrite(data.ScheduledEventsPath);
|
||||
tasks.Add(JsonSerializer.SerializeAsync(eventsStream, data.ScheduledEvents, cancellationToken: ct));
|
||||
|
||||
foreach (var memberData in data.MemberData.Values) {
|
||||
await using var memberDataStream = File.OpenWrite($"{data.MemberDataPath}/{memberData.Id}.json");
|
||||
tasks.Add(JsonSerializer.SerializeAsync(memberDataStream, memberData, cancellationToken: ct));
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
@ -34,11 +52,11 @@ public class GuildDataService : IHostedService {
|
|||
|
||||
private async Task<GuildData> InitializeData(Snowflake guildId, CancellationToken ct = default) {
|
||||
var idString = $"{guildId}";
|
||||
var memberDataDir = $"{guildId}/MemberData";
|
||||
var memberDataPath = $"{guildId}/MemberData";
|
||||
var configurationPath = $"{guildId}/Configuration.json";
|
||||
var scheduledEventsPath = $"{guildId}/ScheduledEvents.json";
|
||||
if (!Directory.Exists(idString)) Directory.CreateDirectory(idString);
|
||||
if (!Directory.Exists(memberDataDir)) Directory.CreateDirectory(memberDataDir);
|
||||
if (!Directory.Exists(memberDataPath)) Directory.CreateDirectory(memberDataPath);
|
||||
if (!File.Exists(configurationPath)) await File.WriteAllTextAsync(configurationPath, "{}", ct);
|
||||
if (!File.Exists(scheduledEventsPath)) await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct);
|
||||
|
||||
|
@ -52,15 +70,32 @@ public class GuildDataService : IHostedService {
|
|||
= JsonSerializer.DeserializeAsync<Dictionary<ulong, ScheduledEventData>>(
|
||||
eventsStream, cancellationToken: ct);
|
||||
|
||||
var data = new GuildData(
|
||||
var memberData = new Dictionary<ulong, MemberData>();
|
||||
foreach (var dataPath in Directory.GetFiles(memberDataPath)) {
|
||||
await using var dataStream = File.OpenRead(dataPath);
|
||||
var data = await JsonSerializer.DeserializeAsync<MemberData>(dataStream, cancellationToken: ct);
|
||||
if (data is null) continue;
|
||||
|
||||
memberData.Add(data.Id, data);
|
||||
}
|
||||
|
||||
var finalData = new GuildData(
|
||||
await configuration ?? new GuildConfiguration(), configurationPath,
|
||||
await events ?? new Dictionary<ulong, ScheduledEventData>(),
|
||||
scheduledEventsPath);
|
||||
_datas.Add(guildId, data);
|
||||
return data;
|
||||
await events ?? new Dictionary<ulong, ScheduledEventData>(), scheduledEventsPath,
|
||||
memberData, memberDataPath);
|
||||
_datas.Add(guildId, finalData);
|
||||
return finalData;
|
||||
}
|
||||
|
||||
public async Task<GuildConfiguration> GetConfiguration(Snowflake guildId, CancellationToken ct = default) {
|
||||
return (await GetData(guildId, ct)).Configuration;
|
||||
}
|
||||
|
||||
public async Task<MemberData> GetMemberData(Snowflake guildId, Snowflake userId, CancellationToken ct = default) {
|
||||
return (await GetData(guildId, ct)).GetMemberData(userId);
|
||||
}
|
||||
|
||||
public List<Snowflake> GetGuildIds() {
|
||||
return _datas.Keys.ToList();
|
||||
}
|
||||
}
|
||||
|
|
41
Services/GuildUpdateService.cs
Normal file
41
Services/GuildUpdateService.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
using Boyfriend.Services.Data;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Remora.Discord.API.Abstractions.Rest;
|
||||
using Remora.Rest.Core;
|
||||
|
||||
namespace Boyfriend.Services;
|
||||
|
||||
public class GuildUpdateService : BackgroundService {
|
||||
private readonly GuildDataService _dataService;
|
||||
private readonly IDiscordRestGuildAPI _guildApi;
|
||||
|
||||
public GuildUpdateService(GuildDataService dataService, IDiscordRestGuildAPI guildApi) {
|
||||
_dataService = dataService;
|
||||
_guildApi = guildApi;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken ct) {
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
|
||||
var tasks = new List<Task>();
|
||||
|
||||
while (await timer.WaitForNextTickAsync(ct)) {
|
||||
foreach (var id in _dataService.GetGuildIds())
|
||||
tasks.Add(TickGuildAsync(id, ct));
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
tasks.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TickGuildAsync(Snowflake guildId, CancellationToken ct = default) {
|
||||
var data = await _dataService.GetData(guildId, ct);
|
||||
Messages.Culture = data.Culture;
|
||||
|
||||
foreach (var memberData in data.MemberData.Values)
|
||||
if (DateTimeOffset.UtcNow > memberData.BannedUntil) {
|
||||
_ = _guildApi.RemoveGuildBanAsync(
|
||||
guildId, memberData.Id.ToDiscordSnowflake(), Messages.PunishmentExpired.EncodeHeader(), ct);
|
||||
memberData.BannedUntil = null;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue