1
0
Fork 1
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:
Octol1ttle 2023-06-11 16:57:19 +05:00
parent e883e143eb
commit 9c080d9691
Signed by: Octol1ttle
GPG key ID: B77C34313AEE1FFF
10 changed files with 144 additions and 31 deletions

View file

@ -74,6 +74,7 @@ public class Boyfriend {
.AddInteractionGroup<InteractionResponders>()
.AddSingleton<GuildDataService>()
.AddSingleton<UtilityService>()
.AddHostedService<GuildUpdateService>()
.AddCommandTree()
.WithCommandGroup<BanCommandGroup>();
var responderTypes = typeof(Boyfriend).Assembly

View file

@ -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);

View file

@ -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];
}
}

View file

@ -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
View 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; }
}

View file

@ -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; }
}

View file

@ -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;

View file

@ -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)))

View file

@ -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();
}
}

View 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;
}
}
}