1
0
Fork 1
mirror of https://github.com/TeamOctolings/Octobot.git synced 2025-04-19 16:33:36 +03:00

Keep adapting code to new guild data storage...

Fix #15, update InspectCode, make Dependabot use the correct label
This commit is contained in:
Octol1ttle 2023-01-12 22:00:52 +05:00
parent fe2cfb3b3c
commit 4b2d98c440
Signed by: Octol1ttle
GPG key ID: B77C34313AEE1FFF
16 changed files with 152 additions and 131 deletions

View file

@ -13,7 +13,9 @@ updates:
# Allow both direct and indirect updates for all packages
- dependency-type: "all"
assignees:
- "l1ttleO"
- "Octol1ttle"
labels:
- "type: dependencies"
- package-ecosystem: "nuget" # See documentation for possible values
directory: "/Boyfriend" # Location of package manifests
@ -24,4 +26,6 @@ updates:
- dependency-type: "all"
# Add assignees
assignees:
- "l1ttleO"
- "Octol1ttle"
labels:
- "type: dependencies"

View file

@ -29,7 +29,7 @@ jobs:
run: dotnet restore
- name: ReSharper CLI InspectCode
uses: muno92/resharper_inspectcode@1.6.0
uses: muno92/resharper_inspectcode@1.6.6
with:
solutionPath: ./Boyfriend-CSharp.sln
ignoreIssueType: InvertIf

View file

@ -112,19 +112,30 @@ public sealed class CommandProcessor {
return null;
}
public SocketUser? GetUser(string[] args, string[] cleanArgs, int index, string? argument) {
public Tuple<ulong, SocketUser?>? GetUser(string[] args, string[] cleanArgs, int index) {
if (index >= args.Length) {
Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}",
Context.Message);
return null;
}
var user = Boyfriend.Client.GetUser(Utils.ParseMention(args[index]));
if (user is null && argument is not null)
var mention = Utils.ParseMention(args[index]);
if (mention is 0) {
Utils.SafeAppendToBuilder(_stackedReplyMessage,
$"{ReplyEmojis.InvalidArgument} {string.Format(Messages.InvalidUser, Utils.Wrap(cleanArgs[index]))}",
Context.Message);
return user;
return null;
}
var exists = Utils.UserExists(mention);
if (!exists) {
Utils.SafeAppendToBuilder(_stackedReplyMessage,
$"{ReplyEmojis.Error} {string.Format(Messages.UserNotFound, Utils.Wrap(cleanArgs[index]))}",
Context.Message);
return null;
}
return Tuple.Create(mention, (SocketUser?)Boyfriend.Client.GetUser(mention));
}
public bool HasPermission(GuildPermission permission) {
@ -135,7 +146,7 @@ public sealed class CommandProcessor {
return false;
}
if (!Context.Guild.GetUser(Context.User.Id).GuildPermissions.Has(permission)
if (!GetMember().GuildPermissions.Has(permission)
&& Context.Guild.OwnerId != Context.User.Id) {
Utils.SafeAppendToBuilder(_stackedReplyMessage,
$"{ReplyEmojis.NoPermission} {Utils.GetMessage($"UserCannot{permission}")}",
@ -146,8 +157,12 @@ public sealed class CommandProcessor {
return true;
}
public SocketGuildUser? GetMember(SocketUser user) {
return Context.Guild.GetUser(user.Id);
private SocketGuildUser GetMember() {
return GetMember(Context.User.Id)!;
}
public SocketGuildUser? GetMember(ulong id) {
return Context.Guild.GetUser(id);
}
public SocketGuildUser? GetMember(string[] args, string[] cleanArgs, int index, string? argument) {
@ -165,10 +180,6 @@ public sealed class CommandProcessor {
return member;
}
private SocketGuildUser GetMember() {
return Context.Guild.GetUser(Context.User.Id);
}
public ulong? GetBan(string[] args, int index) {
if (index >= args.Length) {
Utils.SafeAppendToBuilder(_stackedReplyMessage, $"{ReplyEmojis.MissingArgument} {Messages.MissingUser}",

View file

@ -8,10 +8,10 @@ public sealed class BanCommand : ICommand {
public string[] Aliases { get; } = { "ban", "бан" };
public async Task RunAsync(CommandProcessor cmd, string[] args, string[] cleanArgs) {
var toBan = cmd.GetUser(args, cleanArgs, 0, "ToBan");
var toBan = cmd.GetUser(args, cleanArgs, 0);
if (toBan is null || !cmd.HasPermission(GuildPermission.BanMembers)) return;
var memberToBan = cmd.GetMember(toBan);
var memberToBan = cmd.GetMember(toBan.Item1);
if (memberToBan is not null && !cmd.CanInteractWith(memberToBan, "Ban")) return;
var duration = CommandProcessor.GetTimeSpan(args, 1);
@ -19,21 +19,23 @@ public sealed class BanCommand : ICommand {
if (reason is not null) await BanUserAsync(cmd, toBan, duration, reason);
}
private static async Task BanUserAsync(CommandProcessor cmd, SocketUser toBan, TimeSpan duration, string reason) {
private static async Task BanUserAsync(CommandProcessor cmd, Tuple<ulong, SocketUser?> toBan, TimeSpan duration,
string reason) {
var author = cmd.Context.User;
var guild = cmd.Context.Guild;
await Utils.SendDirectMessage(toBan,
string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.Wrap(reason)));
if (toBan.Item2 is not null)
await Utils.SendDirectMessage(toBan.Item2,
string.Format(Messages.YouWereBanned, author.Mention, guild.Name, Utils.Wrap(reason)));
var guildBanMessage = $"({author}) {reason}";
await guild.AddBanAsync(toBan, 0, guildBanMessage);
await guild.AddBanAsync(toBan.Item1, 0, guildBanMessage);
var memberData = GuildData.FromSocketGuild(guild).MemberData[toBan.Id];
var memberData = GuildData.FromSocketGuild(guild).MemberData[toBan.Item1];
memberData.BannedUntil
= duration.TotalSeconds < 1 ? -1 : DateTimeOffset.Now.Add(duration).ToUnixTimeSeconds();
= duration.TotalSeconds < 1 ? DateTimeOffset.MaxValue : DateTimeOffset.Now.Add(duration);
memberData.Roles.Clear();
var feedback = string.Format(Messages.FeedbackUserBanned, toBan.Mention,
var feedback = string.Format(Messages.FeedbackUserBanned, $"<@{toBan.Item1.ToString()}>",
Utils.GetHumanizedTimeOffset(duration), Utils.Wrap(reason));
cmd.Reply(feedback, ReplyEmojis.Banned);
cmd.Audit(feedback);

View file

@ -19,8 +19,8 @@ public sealed class MuteCommand : ICommand {
if ((role is not null && toMute.Roles.Contains(role))
|| (toMute.TimedOutUntil is not null
&& toMute.TimedOutUntil.Value.ToUnixTimeSeconds()
> DateTimeOffset.Now.ToUnixTimeSeconds())) {
&& toMute.TimedOutUntil.Value
> DateTimeOffset.Now)) {
cmd.Reply(Messages.MemberAlreadyMuted, ReplyEmojis.Error);
return;
}
@ -41,7 +41,7 @@ public sealed class MuteCommand : ICommand {
await toMute.AddRoleAsync(role, requestOptions);
data.MemberData[toMute.Id].MutedUntil = DateTimeOffset.Now.Add(duration).ToUnixTimeSeconds();
data.MemberData[toMute.Id].MutedUntil = DateTimeOffset.Now.Add(duration);
} else {
if (!hasDuration || duration.TotalDays > 28) {
cmd.Reply(Messages.DurationRequiredForTimeOuts, ReplyEmojis.Error);

View file

@ -10,7 +10,7 @@ public sealed class RemindCommand : ICommand {
var reminderText = cmd.GetRemaining(args, 1, "ReminderText");
if (reminderText is not null)
GuildData.FromSocketGuild(cmd.Context.Guild).MemberData[cmd.Context.User.Id].Reminders.Add(new Reminder {
RemindAt = DateTimeOffset.Now.Add(remindIn).ToUnixTimeSeconds(),
RemindAt = DateTimeOffset.Now.Add(remindIn),
ReminderText = reminderText
});

View file

@ -27,8 +27,7 @@ public sealed class UnmuteCommand : ICommand {
await toUnmute.AddRolesAsync(data.MemberData[toUnmute.Id].Roles, requestOptions);
await toUnmute.RemoveRoleAsync(role, requestOptions);
} else {
if (toUnmute.TimedOutUntil is null || toUnmute.TimedOutUntil.Value.ToUnixTimeSeconds() <
DateTimeOffset.Now.ToUnixTimeSeconds()) {
if (toUnmute.TimedOutUntil is null || toUnmute.TimedOutUntil.Value < DateTimeOffset.Now) {
cmd.Reply(Messages.MemberNotMuted, ReplyEmojis.Error);
return;
}

View file

@ -24,7 +24,7 @@ public record GuildData {
// TODO: { "AutoStartEvents", "false" }
};
private static readonly Dictionary<ulong, GuildData> GuildDataDictionary = new();
public static readonly Dictionary<ulong, GuildData> GuildDataDictionary = new();
public readonly Dictionary<ulong, MemberData> MemberData;
@ -44,15 +44,12 @@ public record GuildData {
= JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText($"{_id}/Configuration.json")) ??
new Dictionary<string, string>();
// ReSharper disable twice ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
if (Preferences.Keys.Count < DefaultPreferences.Keys.Count)
foreach (var key in DefaultPreferences.Keys)
if (!Preferences.ContainsKey(key))
Preferences.Add(key, DefaultPreferences[key]);
foreach (var key in DefaultPreferences.Keys.Where(key => !Preferences.ContainsKey(key)))
Preferences.Add(key, DefaultPreferences[key]);
if (Preferences.Keys.Count > DefaultPreferences.Keys.Count)
foreach (var key in Preferences.Keys)
if (!DefaultPreferences.ContainsKey(key))
Preferences.Remove(key);
foreach (var key in Preferences.Keys.Where(key => !DefaultPreferences.ContainsKey(key)))
Preferences.Remove(key);
Preferences.TrimExcess();
MemberData = new Dictionary<ulong, MemberData>();
@ -67,7 +64,7 @@ public record GuildData {
if (MemberData.TryGetValue(member.Id, out var memberData)) {
if (!memberData.IsInGuild &&
DateTimeOffset.Now.ToUnixTimeSeconds() -
Math.Max(memberData.LeftAt.Last(), memberData.BannedUntil) >
Math.Max(memberData.LeftAt.Last().ToUnixTimeSeconds(), memberData.BannedUntil.ToUnixTimeSeconds()) >
60 * 60 * 24 * 30) {
File.Delete($"{_id}/MemberData/{memberData.Id}.json");
MemberData.Remove(memberData.Id);
@ -83,8 +80,11 @@ public record GuildData {
}
public SocketRole? MuteRole {
get => _cachedMuteRole ??= Boyfriend.Client.GetGuild(_id).Roles
.Single(x => x.Id == ulong.Parse(Preferences["MuteRole"]));
get {
if (Preferences["MuteRole"] is "0") return null;
return _cachedMuteRole ??= Boyfriend.Client.GetGuild(_id).Roles
.Single(x => x.Id == ulong.Parse(Preferences["MuteRole"]));
}
set => _cachedMuteRole = value;
}

View file

@ -3,23 +3,23 @@
namespace Boyfriend.Data;
public record MemberData {
public long BannedUntil;
public DateTimeOffset BannedUntil;
public ulong Id;
public bool IsInGuild;
public List<long> JoinedAt;
public List<long> LeftAt;
public long MutedUntil;
public List<DateTimeOffset> JoinedAt;
public List<DateTimeOffset> LeftAt;
public DateTimeOffset MutedUntil;
public List<Reminder> Reminders;
public List<ulong> Roles;
public MemberData(IGuildUser user) {
Id = user.Id;
IsInGuild = true;
JoinedAt = new List<long> { user.JoinedAt!.Value.ToUnixTimeSeconds() };
LeftAt = new List<long>();
JoinedAt = new List<DateTimeOffset> { user.JoinedAt!.Value };
LeftAt = new List<DateTimeOffset>();
Roles = user.RoleIds.ToList();
Reminders = new List<Reminder>();
MutedUntil = 0;
BannedUntil = 0;
MutedUntil = DateTimeOffset.MinValue;
BannedUntil = DateTimeOffset.MinValue;
}
}

View file

@ -1,6 +1,6 @@
namespace Boyfriend.Data;
public struct Reminder {
public long RemindAt;
public DateTimeOffset RemindAt;
public string ReminderText;
}

View file

@ -110,19 +110,18 @@ public static class EventHandler {
if (!data.MemberData.ContainsKey(user.Id)) data.MemberData.Add(user.Id, new MemberData(user));
var memberData = data.MemberData[user.Id];
memberData.IsInGuild = true;
memberData.BannedUntil = 0;
memberData.BannedUntil = DateTimeOffset.MinValue;
if (memberData.LeftAt.Count > 0) {
if (memberData.JoinedAt.Contains(user.JoinedAt!.Value.ToUnixTimeSeconds()))
if (memberData.JoinedAt.Contains(user.JoinedAt!.Value))
throw new UnreachableException();
memberData.JoinedAt.Add(user.JoinedAt!.Value.ToUnixTimeSeconds());
memberData.JoinedAt.Add(user.JoinedAt!.Value);
}
if (memberData.MutedUntil < DateTimeOffset.Now.ToUnixTimeSeconds()) {
if (memberData.MutedUntil < DateTimeOffset.Now) {
if (data.MuteRole is not null)
await user.AddRoleAsync(data.MuteRole);
else
await user.SetTimeOutAsync(
TimeSpan.FromSeconds(DateTimeOffset.Now.ToUnixTimeSeconds() - memberData.MutedUntil));
await user.SetTimeOutAsync(DateTimeOffset.Now - memberData.MutedUntil);
if (config["RemoveRolesOnMute"] is "false" && config["ReturnRolesOnRejoin"] is "true")
await user.AddRolesAsync(memberData.Roles);
@ -132,11 +131,10 @@ public static class EventHandler {
private static Task UserLeftEvent(SocketGuild guild, SocketUser user) {
var data = GuildData.FromSocketGuild(guild).MemberData[user.Id];
data.IsInGuild = false;
data.LeftAt.Add(DateTimeOffset.Now.ToUnixTimeSeconds());
data.LeftAt.Add(DateTimeOffset.Now);
return Task.CompletedTask;
}
// TODO: store data about event (early) notifications
private static async Task ScheduledEventCreatedEvent(SocketGuildEvent scheduledEvent) {
var guild = scheduledEvent.Guild;
var eventConfig = GuildData.FromSocketGuild(guild).Preferences;

View file

@ -1050,11 +1050,11 @@ namespace Boyfriend {
}
/// <summary>
/// Looks up a localized string similar to The specified user is not a member of this server!.
/// Looks up a localized string similar to .
/// </summary>
internal static string UserNotInGuild {
internal static string UserNotFound {
get {
return ResourceManager.GetString("UserNotInGuild", resourceCulture);
return ResourceManager.GetString("UserNotFound", resourceCulture);
}
}

View file

@ -1,64 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
<!--
Microsoft ResX Schema
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
@ -225,9 +225,6 @@
<data name="FeedbackUserBanned" xml:space="preserve">
<value>Banned {0} for{1}: {2}</value>
</data>
<data name="UserNotInGuild" xml:space="preserve">
<value>The specified user is not a member of this server!</value>
</data>
<data name="SettingDoesntExist" xml:space="preserve">
<value>That setting doesn't exist!</value>
</data>
@ -450,13 +447,16 @@
<data name="BotCannotUnmuteTarget" xml:space="preserve">
<value>I cannot unmute this member!</value>
</data>
<data name="UserCannotUnmuteTarget" xml:space="preserve">
<data name="UserCannotUnmuteTarget" xml:space="preserve">
<value>You cannot unmute this user!</value>
</data>
<data name="EventEarlyNotification" xml:space="preserve">
<data name="EventEarlyNotification" xml:space="preserve">
<value>{0}Event {1} will start &lt;t:{2}:R&gt;!</value>
</data>
<data name="SettingsEventEarlyNotificationOffset" xml:space="preserve">
<data name="SettingsEventEarlyNotificationOffset" xml:space="preserve">
<value>Early event start notification offset</value>
</data>
</root>
<data name="UserNotFound" xml:space="preserve">
<value/>
</data>
</root>

View file

@ -222,9 +222,6 @@
<data name="FeedbackUserBanned" xml:space="preserve">
<value>Забанен {0} на{1}: {2}</value>
</data>
<data name="UserNotInGuild" xml:space="preserve">
<value>Указанный пользователь не является участником этого сервера!</value>
</data>
<data name="SettingDoesntExist" xml:space="preserve">
<value>Такая настройка не существует!</value>
</data>

View file

@ -222,9 +222,6 @@
<data name="FeedbackUserBanned" xml:space="preserve">
<value>забанен {0} на{1}: {2}</value>
</data>
<data name="UserNotInGuild" xml:space="preserve">
<value>шизик не на этом сервере</value>
</data>
<data name="SettingDoesntExist" xml:space="preserve">
<value>такой прикол не существует</value>
</data>

View file

@ -49,6 +49,11 @@ public static partial class Utils {
return ulong.TryParse(NumbersOnlyRegex().Replace(mention, ""), out var id) ? id : 0;
}
public static async Task SendDirectMessage(ulong id, string toSend) {
if (await Boyfriend.Client.GetUserAsync(id) is SocketUser user)
await SendDirectMessage(user, toSend);
}
public static async Task SendDirectMessage(SocketUser user, string toSend) {
try { await user.SendMessageAsync(toSend); } catch (HttpException e) {
if (e.DiscordCode is not DiscordErrorCode.CannotSendMessageToUser) throw;
@ -133,6 +138,14 @@ public static partial class Utils {
.Preferences["EventNotificationChannel"]));
}
public static bool UserExists(ulong id) {
return Boyfriend.Client.GetUser(id) is not null || UserInMemberData(id);
}
private static bool UserInMemberData(ulong id) {
return GuildData.GuildDataDictionary.Values.Any(gData => gData.MemberData.Values.Any(mData => mData.Id == id));
}
[GeneratedRegex("[^0-9]")]
private static partial Regex NumbersOnlyRegex();
}