diff --git a/.editorconfig b/.editorconfig
index ff9c068..adbec5a 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -42,7 +42,7 @@ csharp_space_between_square_brackets = false
csharp_style_expression_bodied_accessors = false:warning
csharp_style_expression_bodied_constructors = false:warning
csharp_style_expression_bodied_methods = false:warning
-csharp_style_expression_bodied_properties = false:warning
+csharp_style_expression_bodied_properties = true:warning
csharp_style_namespace_declarations = file_scoped:warning
csharp_style_prefer_utf8_string_literals = true:warning
csharp_style_var_elsewhere = true:warning
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 4545f2b..57eea90 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -15,6 +15,10 @@ updates:
labels:
- "type: change"
- "area: build/ci"
+ # For all packages, ignore all patch updates
+ ignore:
+ - dependency-name: "*"
+ update-types: [ "version-update:semver-patch" ]
- package-ecosystem: "nuget" # See documentation for possible values
directory: "/" # Location of package manifests
@@ -30,3 +34,7 @@ updates:
remora:
patterns:
- "Remora.Discord.*"
+ # For all packages, ignore all patch updates
+ ignore:
+ - dependency-name: "*"
+ update-types: [ "version-update:semver-patch" ]
diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml
index 8002f6f..b2991db 100644
--- a/.github/workflows/build-pr.yml
+++ b/.github/workflows/build-pr.yml
@@ -22,8 +22,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.301'
+
- name: ReSharper CLI InspectCode
- uses: muno92/resharper_inspectcode@1.11.7
+ uses: muno92/resharper_inspectcode@1.11.12
with:
solutionPath: ./Octobot.sln
ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor
diff --git a/Octobot.sln b/Octobot.sln
index 9dd2b89..b82f7a9 100644
--- a/Octobot.sln
+++ b/Octobot.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Octobot", "Octobot.csproj", "{9CA7A44F-167C-46D4-923D-88CE71044144}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamOctolings.Octobot", "TeamOctolings.Octobot\TeamOctolings.Octobot.csproj", "{A1679BA2-3A36-4D98-80C0-EEE771398FBD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -8,9 +8,9 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {9CA7A44F-167C-46D4-923D-88CE71044144}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9CA7A44F-167C-46D4-923D-88CE71044144}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9CA7A44F-167C-46D4-923D-88CE71044144}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9CA7A44F-167C-46D4-923D-88CE71044144}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A1679BA2-3A36-4D98-80C0-EEE771398FBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1679BA2-3A36-4D98-80C0-EEE771398FBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1679BA2-3A36-4D98-80C0-EEE771398FBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1679BA2-3A36-4D98-80C0-EEE771398FBD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/TeamOctolings.Octobot/Attributes/StaticCallersOnlyAttribute.cs b/TeamOctolings.Octobot/Attributes/StaticCallersOnlyAttribute.cs
new file mode 100644
index 0000000..0256f62
--- /dev/null
+++ b/TeamOctolings.Octobot/Attributes/StaticCallersOnlyAttribute.cs
@@ -0,0 +1,8 @@
+namespace TeamOctolings.Octobot.Attributes;
+
+///
+/// Any property marked with should only be accessed by static methods.
+/// Such properties may be used to provide dependencies where it is not possible to acquire them through normal means.
+///
+[AttributeUsage(AttributeTargets.Property)]
+public sealed class StaticCallersOnlyAttribute : Attribute;
diff --git a/TeamOctolings.Octobot/BuildInfo.cs b/TeamOctolings.Octobot/BuildInfo.cs
new file mode 100644
index 0000000..4b9a09f
--- /dev/null
+++ b/TeamOctolings.Octobot/BuildInfo.cs
@@ -0,0 +1,18 @@
+namespace TeamOctolings.Octobot;
+
+public static class BuildInfo
+{
+ public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot";
+
+ public const string IssuesUrl = $"{RepositoryUrl}/issues";
+
+ public const string WikiUrl = $"{RepositoryUrl}/wiki";
+
+ private const string Commit = ThisAssembly.Git.Commit;
+
+ private const string Branch = ThisAssembly.Git.Branch;
+
+ public static bool IsDirty => ThisAssembly.Git.IsDirty;
+
+ public static string Version => IsDirty ? $"{Branch}-{Commit}-dirty" : $"{Branch}-{Commit}";
+}
diff --git a/src/ColorsList.cs b/TeamOctolings.Octobot/ColorsList.cs
similarity index 95%
rename from src/ColorsList.cs
rename to TeamOctolings.Octobot/ColorsList.cs
index cd40313..3b66c0a 100644
--- a/src/ColorsList.cs
+++ b/TeamOctolings.Octobot/ColorsList.cs
@@ -1,6 +1,6 @@
using System.Drawing;
-namespace Octobot;
+namespace TeamOctolings.Octobot;
///
/// Contains all colors used in embeds.
diff --git a/src/Commands/AboutCommandGroup.cs b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs
similarity index 78%
rename from src/Commands/AboutCommandGroup.cs
rename to TeamOctolings.Octobot/Commands/AboutCommandGroup.cs
index e978ec9..dbb8b12 100644
--- a/src/Commands/AboutCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs
@@ -1,9 +1,6 @@
using System.ComponentModel;
using System.Text;
using JetBrains.Annotations;
-using Octobot.Data;
-using Octobot.Extensions;
-using Octobot.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
@@ -18,14 +15,17 @@ using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Services;
-namespace Octobot.Commands;
+namespace TeamOctolings.Octobot.Commands;
///
/// Handles the command to show information about this bot: /about.
///
[UsedImplicitly]
-public class AboutCommandGroup : CommandGroup
+public sealed class AboutCommandGroup : CommandGroup
{
private static readonly (string Username, Snowflake Id)[] Developers =
[
@@ -36,9 +36,9 @@ public class AboutCommandGroup : CommandGroup
private readonly ICommandContext _context;
private readonly IFeedbackService _feedback;
+ private readonly IDiscordRestGuildAPI _guildApi;
private readonly GuildDataService _guildData;
private readonly IDiscordRestUserAPI _userApi;
- private readonly IDiscordRestGuildAPI _guildApi;
public AboutCommandGroup(
ICommandContext context, GuildDataService guildData,
@@ -73,7 +73,7 @@ public class AboutCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var cfg = await _guildData.GetSettings(guildId, CancellationToken);
@@ -100,27 +100,38 @@ public class AboutCommandGroup : CommandGroup
.WithSmallTitle(string.Format(Messages.AboutBot, bot.Username), bot)
.WithDescription(builder.ToString())
.WithColour(ColorsList.Cyan)
- .WithImageUrl("https://i.ibb.co/fS6wZhh/octobot-banner.png")
+ .WithImageUrl("https://raw.githubusercontent.com/TeamOctolings/Octobot/HEAD/docs/octobot-banner.png")
+ .WithFooter(string.Format(Messages.Version, BuildInfo.Version))
.Build();
var repositoryButton = new ButtonComponent(
ButtonComponentStyle.Link,
Messages.ButtonOpenRepository,
- new PartialEmoji(Name: "🌐"),
- URL: Octobot.RepositoryUrl
+ new PartialEmoji(Name: "\ud83c\udf10"), // 'GLOBE WITH MERIDIANS' (U+1F310)
+ URL: BuildInfo.RepositoryUrl
+ );
+
+ var wikiButton = new ButtonComponent(
+ ButtonComponentStyle.Link,
+ Messages.ButtonOpenWiki,
+ new PartialEmoji(Name: "\ud83d\udcd6"), // 'OPEN BOOK' (U+1F4D6)
+ URL: BuildInfo.WikiUrl
);
var issuesButton = new ButtonComponent(
ButtonComponentStyle.Link,
- Messages.ButtonReportIssue,
- new PartialEmoji(Name: "⚠️"),
- URL: Octobot.IssuesUrl
+ BuildInfo.IsDirty
+ ? Messages.ButtonDirty
+ : Messages.ButtonReportIssue,
+ new PartialEmoji(Name: "\u26a0\ufe0f"), // 'WARNING SIGN' (U+26A0)
+ URL: BuildInfo.IssuesUrl,
+ IsDisabled: BuildInfo.IsDirty
);
return await _feedback.SendContextualEmbedResultAsync(embed,
new FeedbackMessageOptions(MessageComponents: new[]
{
- new ActionRowComponent(new[] { repositoryButton, issuesButton })
+ new ActionRowComponent(new[] { repositoryButton, wikiButton, issuesButton })
}), ct);
}
}
diff --git a/src/Commands/BanCommandGroup.cs b/TeamOctolings.Octobot/Commands/BanCommandGroup.cs
similarity index 84%
rename from src/Commands/BanCommandGroup.cs
rename to TeamOctolings.Octobot/Commands/BanCommandGroup.cs
index 6dbf9b9..69be80f 100644
--- a/src/Commands/BanCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/BanCommandGroup.cs
@@ -2,11 +2,6 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text;
using JetBrains.Annotations;
-using Octobot.Data;
-using Octobot.Extensions;
-using Octobot.Parsers;
-using Octobot.Services;
-using Octobot.Services.Update;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
@@ -19,15 +14,21 @@ using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Parsers;
+using TeamOctolings.Octobot.Services;
+using TeamOctolings.Octobot.Services.Update;
-namespace Octobot.Commands;
+namespace TeamOctolings.Octobot.Commands;
///
/// Handles commands related to ban management: /ban and /unban.
///
[UsedImplicitly]
-public class BanCommandGroup : CommandGroup
+public sealed class BanCommandGroup : CommandGroup
{
+ private readonly AccessControlService _access;
private readonly IDiscordRestChannelAPI _channelApi;
private readonly ICommandContext _context;
private readonly IFeedbackService _feedback;
@@ -36,16 +37,16 @@ public class BanCommandGroup : CommandGroup
private readonly IDiscordRestUserAPI _userApi;
private readonly Utility _utility;
- public BanCommandGroup(
- ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData,
- IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi,
- Utility utility)
+ public BanCommandGroup(AccessControlService access, IDiscordRestChannelAPI channelApi, ICommandContext context,
+ IFeedbackService feedback, IDiscordRestGuildAPI guildApi, GuildDataService guildData,
+ IDiscordRestUserAPI userApi, Utility utility)
{
- _context = context;
+ _access = access;
_channelApi = channelApi;
- _guildData = guildData;
+ _context = context;
_feedback = feedback;
_guildApi = guildApi;
+ _guildData = guildData;
_userApi = userApi;
_utility = utility;
}
@@ -65,10 +66,10 @@ public class BanCommandGroup : CommandGroup
///
///
[Command("ban", "бан")]
- [DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)]
+ [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
[DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)]
- [RequireDiscordPermission(DiscordPermission.BanMembers)]
+ [RequireDiscordPermission(DiscordPermission.ManageMessages)]
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
[Description("Ban user")]
[UsedImplicitly]
@@ -76,7 +77,8 @@ public class BanCommandGroup : CommandGroup
[Description("User to ban")] IUser target,
[Description("Ban reason")] [MaxLength(256)]
string reason,
- [Description("Ban duration")] string? duration = null)
+ [Description("Ban duration (e.g. 1h30m)")]
+ string? duration = null)
{
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId))
{
@@ -87,19 +89,19 @@ public class BanCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor))
{
- return Result.FromError(executorResult);
+ return ResultExtensions.FromError(executorResult);
}
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
if (!guildResult.IsDefined(out var guild))
{
- return Result.FromError(guildResult);
+ return ResultExtensions.FromError(guildResult);
}
var data = await _guildData.GetData(guild.ID, CancellationToken);
@@ -116,6 +118,7 @@ public class BanCommandGroup : CommandGroup
{
var failedEmbed = new EmbedBuilder()
.WithSmallTitle(Messages.InvalidTimeSpan, bot)
+ .WithDescription(Messages.TimeSpanExample)
.WithColour(ColorsList.Red)
.Build();
@@ -126,7 +129,8 @@ public class BanCommandGroup : CommandGroup
}
private async Task BanUserAsync(
- IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data, Snowflake channelId,
+ IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data,
+ Snowflake channelId,
IUser bot, CancellationToken ct = default)
{
var existingBanResult = await _guildApi.GetGuildBanAsync(guild.ID, target.ID, ct);
@@ -139,10 +143,10 @@ public class BanCommandGroup : CommandGroup
}
var interactionResult
- = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Ban", ct);
+ = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Ban", ct);
if (!interactionResult.IsSuccess)
{
- return Result.FromError(interactionResult);
+ return ResultExtensions.FromError(interactionResult);
}
if (interactionResult.Entity is not null)
@@ -153,7 +157,8 @@ public class BanCommandGroup : CommandGroup
return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct);
}
- var builder = new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason));
+ var builder =
+ new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason));
if (duration is not null)
{
builder.AppendBulletPoint(
@@ -179,17 +184,19 @@ public class BanCommandGroup : CommandGroup
await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct);
}
+ var memberData = data.GetOrCreateMemberData(target.ID);
+ memberData.BannedUntil
+ = duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue;
+
var banResult = await _guildApi.CreateGuildBanAsync(
guild.ID, target.ID, reason: $"({executor.GetTag()}) {reason}".EncodeHeader(),
ct: ct);
if (!banResult.IsSuccess)
{
- return Result.FromError(banResult.Error);
+ memberData.BannedUntil = null;
+ return ResultExtensions.FromError(banResult);
}
- var memberData = data.GetOrCreateMemberData(target.ID);
- memberData.BannedUntil
- = duration is not null ? DateTimeOffset.UtcNow.Add(duration.Value) : DateTimeOffset.MaxValue;
memberData.Roles.Clear();
var embed = new EmbedBuilder().WithSmallTitle(
@@ -217,10 +224,10 @@ public class BanCommandGroup : CommandGroup
///
///
[Command("unban")]
- [DiscordDefaultMemberPermissions(DiscordPermission.BanMembers)]
+ [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
[DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)]
- [RequireDiscordPermission(DiscordPermission.BanMembers)]
+ [RequireDiscordPermission(DiscordPermission.ManageMessages)]
[RequireBotDiscordPermissions(DiscordPermission.BanMembers)]
[Description("Unban user")]
[UsedImplicitly]
@@ -238,14 +245,14 @@ public class BanCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
// Needed to get the tag and avatar
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor))
{
- return Result.FromError(executorResult);
+ return ResultExtensions.FromError(executorResult);
}
var data = await _guildData.GetData(guildId, CancellationToken);
@@ -272,7 +279,7 @@ public class BanCommandGroup : CommandGroup
ct);
if (!unbanResult.IsSuccess)
{
- return Result.FromError(unbanResult.Error);
+ return ResultExtensions.FromError(unbanResult);
}
data.GetOrCreateMemberData(target.ID).BannedUntil = null;
@@ -282,7 +289,8 @@ public class BanCommandGroup : CommandGroup
.WithColour(ColorsList.Green).Build();
var title = string.Format(Messages.UserUnbanned, target.GetTag());
- var description = new StringBuilder().AppendBulletPoint(string.Format(Messages.DescriptionActionReason, reason));
+ var description =
+ new StringBuilder().AppendBulletPoint(string.Format(Messages.DescriptionActionReason, reason));
_utility.LogAction(
data.Settings, channelId, executor, title, description.ToString(), target, ColorsList.Green, ct: ct);
diff --git a/src/Commands/ClearCommandGroup.cs b/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs
similarity index 74%
rename from src/Commands/ClearCommandGroup.cs
rename to TeamOctolings.Octobot/Commands/ClearCommandGroup.cs
index 395810f..7c1b516 100644
--- a/src/Commands/ClearCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs
@@ -1,9 +1,6 @@
using System.ComponentModel;
using System.Text;
using JetBrains.Annotations;
-using Octobot.Data;
-using Octobot.Extensions;
-using Octobot.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
@@ -16,14 +13,17 @@ using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Services;
-namespace Octobot.Commands;
+namespace TeamOctolings.Octobot.Commands;
///
/// Handles the command to clear messages in a channel: /clear.
///
[UsedImplicitly]
-public class ClearCommandGroup : CommandGroup
+public sealed class ClearCommandGroup : CommandGroup
{
private readonly IDiscordRestChannelAPI _channelApi;
private readonly ICommandContext _context;
@@ -64,6 +64,7 @@ public class ClearCommandGroup : CommandGroup
public async Task ExecuteClear(
[Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)]
int amount,
+ [Description("Ignore messages except from the specified author")]
IUser? author = null)
{
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId))
@@ -75,20 +76,20 @@ public class ClearCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor))
{
- return Result.FromError(executorResult);
+ return ResultExtensions.FromError(executorResult);
}
var messagesResult = await _channelApi.GetChannelMessagesAsync(
channelId, limit: amount + 1, ct: CancellationToken);
if (!messagesResult.IsDefined(out var messages))
{
- return Result.FromError(messagesResult);
+ return ResultExtensions.FromError(messagesResult);
}
var data = await _guildData.GetData(guildId, CancellationToken);
@@ -102,7 +103,9 @@ public class ClearCommandGroup : CommandGroup
CancellationToken ct = default)
{
var idList = new List(messages.Count);
- var builder = new StringBuilder().AppendLine(Mention.Channel(channelId)).AppendLine();
+
+ var logEntries = new List { new() };
+ var currentLogEntry = 0;
for (var i = messages.Count - 1; i >= 1; i--) // '>= 1' to skip last message ('Octobot is thinking...')
{
var message = messages[i];
@@ -112,8 +115,17 @@ public class ClearCommandGroup : CommandGroup
}
idList.Add(message.ID);
- builder.AppendLine(string.Format(Messages.MessageFrom, Mention.User(message.Author)));
- builder.Append(message.Content.InBlockCode());
+
+ var entry = logEntries[currentLogEntry];
+ var str = $"{string.Format(Messages.MessageFrom, Mention.User(message.Author))}\n{message.Content.InBlockCode()}";
+ if (entry.Builder.Length + str.Length > EmbedConstants.MaxDescriptionLength)
+ {
+ logEntries.Add(entry = new ClearedMessageEntry());
+ currentLogEntry++;
+ }
+
+ entry.Builder.Append(str);
+ entry.DeletedCount++;
}
if (idList.Count == 0)
@@ -127,21 +139,32 @@ public class ClearCommandGroup : CommandGroup
var title = author is not null
? string.Format(Messages.MessagesClearedFiltered, idList.Count.ToString(), author.GetTag())
: string.Format(Messages.MessagesCleared, idList.Count.ToString());
- var description = builder.ToString();
var deleteResult = await _channelApi.BulkDeleteMessagesAsync(
channelId, idList, executor.GetTag().EncodeHeader(), ct);
if (!deleteResult.IsSuccess)
{
- return Result.FromError(deleteResult.Error);
+ return ResultExtensions.FromError(deleteResult);
}
- _utility.LogAction(
- data.Settings, channelId, executor, title, description, bot, ColorsList.Red, false, ct);
+ foreach (var log in logEntries)
+ {
+ _utility.LogAction(
+ data.Settings, channelId, executor, author is not null
+ ? string.Format(Messages.MessagesClearedFiltered, log.DeletedCount.ToString(), author.GetTag())
+ : string.Format(Messages.MessagesCleared, log.DeletedCount.ToString()),
+ log.Builder.ToString(), bot, ColorsList.Red, false, ct);
+ }
var embed = new EmbedBuilder().WithSmallTitle(title, bot)
.WithColour(ColorsList.Green).Build();
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
}
+
+ private sealed class ClearedMessageEntry
+ {
+ public StringBuilder Builder { get; } = new();
+ public int DeletedCount { get; set; }
+ }
}
diff --git a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs
similarity index 79%
rename from src/Commands/Events/ErrorLoggingPostExecutionEvent.cs
rename to TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs
index 87cfc84..7409d3b 100644
--- a/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs
+++ b/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs
@@ -1,6 +1,5 @@
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
-using Octobot.Extensions;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Objects;
@@ -11,17 +10,18 @@ using Remora.Discord.Commands.Services;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Results;
+using TeamOctolings.Octobot.Extensions;
-namespace Octobot.Commands.Events;
+namespace TeamOctolings.Octobot.Commands.Events;
///
/// Handles error logging for slash command groups.
///
[UsedImplicitly]
-public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent
+public sealed class ErrorLoggingPostExecutionEvent : IPostExecutionEvent
{
- private readonly ILogger _logger;
private readonly IFeedbackService _feedback;
+ private readonly ILogger _logger;
private readonly IDiscordRestUserAPI _userApi;
public ErrorLoggingPostExecutionEvent(ILogger logger, IFeedbackService feedback,
@@ -53,13 +53,13 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent
if (result.IsSuccess)
{
- return Result.FromSuccess();
+ return Result.Success;
}
var botResult = await _userApi.GetCurrentUserAsync(ct);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var embed = new EmbedBuilder().WithSmallTitle(Messages.CommandExecutionFailed, bot)
@@ -70,15 +70,19 @@ public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent
var issuesButton = new ButtonComponent(
ButtonComponentStyle.Link,
- Messages.ButtonReportIssue,
- new PartialEmoji(Name: "⚠️"),
- URL: Octobot.IssuesUrl
+ BuildInfo.IsDirty
+ ? Messages.ButtonDirty
+ : Messages.ButtonReportIssue,
+ new PartialEmoji(Name: "\u26a0\ufe0f"), // 'WARNING SIGN' (U+26A0)
+ URL: BuildInfo.IssuesUrl,
+ IsDisabled: BuildInfo.IsDirty
);
- return await _feedback.SendContextualEmbedResultAsync(embed,
+ return ResultExtensions.FromError(await _feedback.SendContextualEmbedResultAsync(embed,
new FeedbackMessageOptions(MessageComponents: new[]
{
new ActionRowComponent(new[] { issuesButton })
- }), ct);
+ }), ct)
+ );
}
}
diff --git a/src/Commands/Events/LoggingPreparationErrorEvent.cs b/TeamOctolings.Octobot/Commands/Events/LoggingPreparationErrorEvent.cs
similarity index 85%
rename from src/Commands/Events/LoggingPreparationErrorEvent.cs
rename to TeamOctolings.Octobot/Commands/Events/LoggingPreparationErrorEvent.cs
index be48e74..9e69a7f 100644
--- a/src/Commands/Events/LoggingPreparationErrorEvent.cs
+++ b/TeamOctolings.Octobot/Commands/Events/LoggingPreparationErrorEvent.cs
@@ -1,17 +1,17 @@
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
-using Octobot.Extensions;
using Remora.Discord.Commands.Contexts;
using Remora.Discord.Commands.Services;
using Remora.Results;
+using TeamOctolings.Octobot.Extensions;
-namespace Octobot.Commands.Events;
+namespace TeamOctolings.Octobot.Commands.Events;
///
/// Handles error logging for slash commands that couldn't be successfully prepared.
///
[UsedImplicitly]
-public class LoggingPreparationErrorEvent : IPreparationErrorEvent
+public sealed class LoggingPreparationErrorEvent : IPreparationErrorEvent
{
private readonly ILogger _logger;
@@ -33,6 +33,6 @@ public class LoggingPreparationErrorEvent : IPreparationErrorEvent
{
_logger.LogResult(preparationResult, "Error in slash command preparation.");
- return Task.FromResult(Result.FromSuccess());
+ return Task.FromResult(Result.Success);
}
}
diff --git a/src/Commands/ToolsCommandGroup.cs b/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs
similarity index 55%
rename from src/Commands/ToolsCommandGroup.cs
rename to TeamOctolings.Octobot/Commands/InfoCommandGroup.cs
index 3c16232..f07b210 100644
--- a/src/Commands/ToolsCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs
@@ -2,10 +2,6 @@ using System.ComponentModel;
using System.Drawing;
using System.Text;
using JetBrains.Annotations;
-using Octobot.Data;
-using Octobot.Extensions;
-using Octobot.Parsers;
-using Octobot.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
@@ -17,14 +13,17 @@ using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Services;
-namespace Octobot.Commands;
+namespace TeamOctolings.Octobot.Commands;
///
-/// Handles tool commands: /userinfo, /guildinfo, /random, /timestamp, /8ball.
+/// Handles info commands: /userinfo, /guildinfo.
///
[UsedImplicitly]
-public class ToolsCommandGroup : CommandGroup
+public sealed class InfoCommandGroup : CommandGroup
{
private readonly ICommandContext _context;
private readonly IFeedbackService _feedback;
@@ -32,10 +31,10 @@ public class ToolsCommandGroup : CommandGroup
private readonly GuildDataService _guildData;
private readonly IDiscordRestUserAPI _userApi;
- public ToolsCommandGroup(
+ public InfoCommandGroup(
ICommandContext context, IFeedbackService feedback,
GuildDataService guildData, IDiscordRestGuildAPI guildApi,
- IDiscordRestUserAPI userApi, IDiscordRestChannelAPI channelApi)
+ IDiscordRestUserAPI userApi)
{
_context = context;
_guildData = guildData;
@@ -81,13 +80,13 @@ public class ToolsCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor))
{
- return Result.FromError(executorResult);
+ return ResultExtensions.FromError(executorResult);
}
var data = await _guildData.GetData(guildId, CancellationToken);
@@ -262,7 +261,7 @@ public class ToolsCommandGroup : CommandGroup
///
[Command("guildinfo")]
[DiscordDefaultDMPermission(false)]
- [Description("Shows info current guild")]
+ [Description("Shows info about current guild")]
[UsedImplicitly]
public async Task ExecuteGuildInfoAsync()
{
@@ -274,13 +273,13 @@ public class ToolsCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
if (!guildResult.IsDefined(out var guild))
{
- return Result.FromError(guildResult);
+ return ResultExtensions.FromError(guildResult);
}
var data = await _guildData.GetData(guildId, CancellationToken);
@@ -289,7 +288,7 @@ public class ToolsCommandGroup : CommandGroup
return await ShowGuildInfoAsync(bot, guild, CancellationToken);
}
- private Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct)
+ private Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct = default)
{
var description = new StringBuilder().AppendLine($"## {guild.Name}");
@@ -327,234 +326,4 @@ public class ToolsCommandGroup : CommandGroup
return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
}
-
- ///
- /// A slash command that generates a random number using maximum and minimum numbers.
- ///
- /// The first number used for randomization.
- /// The second number used for randomization. Default value: 0
- ///
- /// A feedback sending result which may or may not have succeeded.
- ///
- [Command("random")]
- [DiscordDefaultDMPermission(false)]
- [Description("Generates a random number")]
- [UsedImplicitly]
- public async Task ExecuteRandomAsync(
- [Description("First number")] long first,
- [Description("Second number (Default: 0)")]
- long? second = null)
- {
- if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId))
- {
- return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
- }
-
- var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
- if (!executorResult.IsDefined(out var executor))
- {
- return Result.FromError(executorResult);
- }
-
- var data = await _guildData.GetData(guildId, CancellationToken);
- Messages.Culture = GuildSettings.Language.Get(data.Settings);
-
- return await SendRandomNumberAsync(first, second, executor, CancellationToken);
- }
-
- private Task SendRandomNumberAsync(long first, long? secondNullable,
- IUser executor, CancellationToken ct)
- {
- const long secondDefault = 0;
- var second = secondNullable ?? secondDefault;
-
- var min = Math.Min(first, second);
- var max = Math.Max(first, second);
-
- var i = Random.Shared.NextInt64(min, max + 1);
-
- var description = new StringBuilder().Append("# ").Append(i);
-
- description.AppendLine().AppendBulletPoint(string.Format(
- Messages.RandomMin, Markdown.InlineCode(min.ToString())));
- if (secondNullable is null && first >= secondDefault)
- {
- description.Append(' ').Append(Messages.Default);
- }
-
- description.AppendLine().AppendBulletPoint(string.Format(
- Messages.RandomMax, Markdown.InlineCode(max.ToString())));
- if (secondNullable is null && first < secondDefault)
- {
- description.Append(' ').Append(Messages.Default);
- }
-
- var embedColor = ColorsList.Blue;
- if (secondNullable is not null && min == max)
- {
- description.AppendLine().Append(Markdown.Italicise(Messages.RandomMinMaxSame));
- embedColor = ColorsList.Red;
- }
-
- var embed = new EmbedBuilder().WithSmallTitle(
- string.Format(Messages.RandomTitle, executor.GetTag()), executor)
- .WithDescription(description.ToString())
- .WithColour(embedColor)
- .Build();
-
- return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
- }
-
- private static readonly TimestampStyle[] AllStyles =
- [
- TimestampStyle.ShortDate,
- TimestampStyle.LongDate,
- TimestampStyle.ShortTime,
- TimestampStyle.LongTime,
- TimestampStyle.ShortDateTime,
- TimestampStyle.LongDateTime,
- TimestampStyle.RelativeTime
- ];
-
- ///
- /// A slash command that shows the current timestamp with an optional offset in all styles supported by Discord.
- ///
- /// The offset for the current timestamp.
- ///
- /// A feedback sending result which may or may not have succeeded.
- ///
- [Command("timestamp")]
- [DiscordDefaultDMPermission(false)]
- [Description("Shows a timestamp in all styles")]
- [UsedImplicitly]
- public async Task ExecuteTimestampAsync(
- [Description("Offset from current time")] [Option("offset")]
- string? stringOffset = null)
- {
- if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId))
- {
- return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
- }
-
- var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
- if (!botResult.IsDefined(out var bot))
- {
- return Result.FromError(botResult);
- }
-
- var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
- if (!executorResult.IsDefined(out var executor))
- {
- return Result.FromError(executorResult);
- }
-
- var data = await _guildData.GetData(guildId, CancellationToken);
- Messages.Culture = GuildSettings.Language.Get(data.Settings);
-
- if (stringOffset is null)
- {
- return await SendTimestampAsync(null, executor, CancellationToken);
- }
-
- var parseResult = TimeSpanParser.TryParse(stringOffset);
- if (!parseResult.IsDefined(out var offset))
- {
- var failedEmbed = new EmbedBuilder()
- .WithSmallTitle(Messages.InvalidTimeSpan, bot)
- .WithColour(ColorsList.Red)
- .Build();
-
- return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken);
- }
-
- return await SendTimestampAsync(offset, executor, CancellationToken);
- }
-
- private Task SendTimestampAsync(TimeSpan? offset, IUser executor, CancellationToken ct)
- {
- var timestamp = DateTimeOffset.UtcNow.Add(offset ?? TimeSpan.Zero).ToUnixTimeSeconds();
-
- var description = new StringBuilder().Append("# ").AppendLine(timestamp.ToString());
-
- if (offset is not null)
- {
- description.AppendLine(string.Format(
- Messages.TimestampOffset, Markdown.InlineCode(offset.ToString() ?? string.Empty))).AppendLine();
- }
-
- foreach (var markdownTimestamp in AllStyles.Select(style => Markdown.Timestamp(timestamp, style)))
- {
- description.AppendBulletPoint(Markdown.InlineCode(markdownTimestamp))
- .Append(" → ").AppendLine(markdownTimestamp);
- }
-
- var embed = new EmbedBuilder().WithSmallTitle(
- string.Format(Messages.TimestampTitle, executor.GetTag()), executor)
- .WithDescription(description.ToString())
- .WithColour(ColorsList.Blue)
- .Build();
-
- return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
- }
-
- ///
- /// A slash command that shows a random answer from the Magic 8-Ball.
- ///
- /// Unused input.
- ///
- /// The 8-Ball answers were taken from Wikipedia.
- ///
- ///
- /// A feedback sending result which may or may not have succeeded.
- ///
- [Command("8ball")]
- [DiscordDefaultDMPermission(false)]
- [Description("Ask the Magic 8-Ball a question")]
- [UsedImplicitly]
- public async Task ExecuteEightBallAsync(
- // let the user think he's actually asking the ball a question
- string question)
- {
- if (!_context.TryGetContextIDs(out var guildId, out _, out _))
- {
- return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
- }
-
- var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
- if (!botResult.IsDefined(out var bot))
- {
- return Result.FromError(botResult);
- }
-
- var data = await _guildData.GetData(guildId, CancellationToken);
- Messages.Culture = GuildSettings.Language.Get(data.Settings);
-
- return await AnswerEightBallAsync(bot, CancellationToken);
- }
-
- private static readonly string[] AnswerTypes =
- [
- "Positive", "Questionable", "Neutral", "Negative"
- ];
-
- private Task AnswerEightBallAsync(IUser bot, CancellationToken ct)
- {
- var typeNumber = Random.Shared.Next(0, 4);
- var embedColor = typeNumber switch
- {
- 0 => ColorsList.Blue,
- 1 => ColorsList.Green,
- 2 => ColorsList.Yellow,
- 3 => ColorsList.Red,
- _ => throw new ArgumentOutOfRangeException(null, nameof(typeNumber))
- };
-
- var answer = $"EightBall{AnswerTypes[typeNumber]}{Random.Shared.Next(1, 6)}".Localized();
-
- var embed = new EmbedBuilder().WithSmallTitle(answer, bot)
- .WithColour(embedColor)
- .Build();
-
- return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
- }
}
diff --git a/src/Commands/KickCommandGroup.cs b/TeamOctolings.Octobot/Commands/KickCommandGroup.cs
similarity index 82%
rename from src/Commands/KickCommandGroup.cs
rename to TeamOctolings.Octobot/Commands/KickCommandGroup.cs
index 0faa1d3..a8fea2a 100644
--- a/src/Commands/KickCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/KickCommandGroup.cs
@@ -1,9 +1,6 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using JetBrains.Annotations;
-using Octobot.Data;
-using Octobot.Extensions;
-using Octobot.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
@@ -15,15 +12,19 @@ using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Extensions.Embeds;
using Remora.Rest.Core;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Services;
-namespace Octobot.Commands;
+namespace TeamOctolings.Octobot.Commands;
///
/// Handles the command to kick members of a guild: /kick.
///
[UsedImplicitly]
-public class KickCommandGroup : CommandGroup
+public sealed class KickCommandGroup : CommandGroup
{
+ private readonly AccessControlService _access;
private readonly IDiscordRestChannelAPI _channelApi;
private readonly ICommandContext _context;
private readonly IFeedbackService _feedback;
@@ -32,16 +33,16 @@ public class KickCommandGroup : CommandGroup
private readonly IDiscordRestUserAPI _userApi;
private readonly Utility _utility;
- public KickCommandGroup(
- ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData,
- IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi,
- Utility utility)
+ public KickCommandGroup(AccessControlService access, IDiscordRestChannelAPI channelApi, ICommandContext context,
+ IFeedbackService feedback, IDiscordRestGuildAPI guildApi, GuildDataService guildData,
+ IDiscordRestUserAPI userApi, Utility utility)
{
- _context = context;
+ _access = access;
_channelApi = channelApi;
- _guildData = guildData;
+ _context = context;
_feedback = feedback;
_guildApi = guildApi;
+ _guildData = guildData;
_userApi = userApi;
_utility = utility;
}
@@ -59,10 +60,10 @@ public class KickCommandGroup : CommandGroup
/// was kicked and vice-versa.
///
[Command("kick", "кик")]
- [DiscordDefaultMemberPermissions(DiscordPermission.KickMembers)]
+ [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
[DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)]
- [RequireDiscordPermission(DiscordPermission.KickMembers)]
+ [RequireDiscordPermission(DiscordPermission.ManageMessages)]
[RequireBotDiscordPermissions(DiscordPermission.KickMembers)]
[Description("Kick member")]
[UsedImplicitly]
@@ -80,19 +81,19 @@ public class KickCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor))
{
- return Result.FromError(executorResult);
+ return ResultExtensions.FromError(executorResult);
}
var guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
if (!guildResult.IsDefined(out var guild))
{
- return Result.FromError(guildResult);
+ return ResultExtensions.FromError(guildResult);
}
var data = await _guildData.GetData(guildId, CancellationToken);
@@ -115,10 +116,10 @@ public class KickCommandGroup : CommandGroup
CancellationToken ct = default)
{
var interactionResult
- = await _utility.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Kick", ct);
+ = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Kick", ct);
if (!interactionResult.IsSuccess)
{
- return Result.FromError(interactionResult);
+ return ResultExtensions.FromError(interactionResult);
}
if (interactionResult.Entity is not null)
@@ -134,7 +135,8 @@ public class KickCommandGroup : CommandGroup
{
var dmEmbed = new EmbedBuilder().WithGuildTitle(guild)
.WithTitle(Messages.YouWereKicked)
- .WithDescription(MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)))
+ .WithDescription(
+ MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason)))
.WithActionFooter(executor)
.WithCurrentTimestamp()
.WithColour(ColorsList.Red)
@@ -143,17 +145,19 @@ public class KickCommandGroup : CommandGroup
await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct);
}
+ var memberData = data.GetOrCreateMemberData(target.ID);
+ memberData.Kicked = true;
+
var kickResult = await _guildApi.RemoveGuildMemberAsync(
guild.ID, target.ID, $"({executor.GetTag()}) {reason}".EncodeHeader(),
ct);
if (!kickResult.IsSuccess)
{
- return Result.FromError(kickResult.Error);
+ memberData.Kicked = false;
+ return ResultExtensions.FromError(kickResult);
}
- var memberData = data.GetOrCreateMemberData(target.ID);
memberData.Roles.Clear();
- memberData.Kicked = true;
var title = string.Format(Messages.UserKicked, target.GetTag());
var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason));
diff --git a/src/Commands/MuteCommandGroup.cs b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs
similarity index 85%
rename from src/Commands/MuteCommandGroup.cs
rename to TeamOctolings.Octobot/Commands/MuteCommandGroup.cs
index 788eb2c..46e8d84 100644
--- a/src/Commands/MuteCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs
@@ -2,11 +2,6 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text;
using JetBrains.Annotations;
-using Octobot.Data;
-using Octobot.Extensions;
-using Octobot.Parsers;
-using Octobot.Services;
-using Octobot.Services.Update;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
@@ -19,15 +14,21 @@ using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Parsers;
+using TeamOctolings.Octobot.Services;
+using TeamOctolings.Octobot.Services.Update;
-namespace Octobot.Commands;
+namespace TeamOctolings.Octobot.Commands;
///
/// Handles commands related to mute management: /mute and /unmute.
///
[UsedImplicitly]
-public class MuteCommandGroup : CommandGroup
+public sealed class MuteCommandGroup : CommandGroup
{
+ private readonly AccessControlService _access;
private readonly ICommandContext _context;
private readonly IFeedbackService _feedback;
private readonly IDiscordRestGuildAPI _guildApi;
@@ -35,14 +36,14 @@ public class MuteCommandGroup : CommandGroup
private readonly IDiscordRestUserAPI _userApi;
private readonly Utility _utility;
- public MuteCommandGroup(
- ICommandContext context, GuildDataService guildData, IFeedbackService feedback,
- IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, Utility utility)
+ public MuteCommandGroup(AccessControlService access, ICommandContext context, IFeedbackService feedback,
+ IDiscordRestGuildAPI guildApi, GuildDataService guildData, IDiscordRestUserAPI userApi, Utility utility)
{
+ _access = access;
_context = context;
- _guildData = guildData;
_feedback = feedback;
_guildApi = guildApi;
+ _guildData = guildData;
_userApi = userApi;
_utility = utility;
}
@@ -62,10 +63,10 @@ public class MuteCommandGroup : CommandGroup
///
///
[Command("mute", "мут")]
- [DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)]
+ [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
[DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)]
- [RequireDiscordPermission(DiscordPermission.ModerateMembers)]
+ [RequireDiscordPermission(DiscordPermission.ManageMessages)]
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
[Description("Mute member")]
[UsedImplicitly]
@@ -73,7 +74,7 @@ public class MuteCommandGroup : CommandGroup
[Description("Member to mute")] IUser target,
[Description("Mute reason")] [MaxLength(256)]
string reason,
- [Description("Mute duration")] [Option("duration")]
+ [Description("Mute duration (e.g. 1h30m)")] [Option("duration")]
string stringDuration)
{
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId))
@@ -85,13 +86,13 @@ public class MuteCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor))
{
- return Result.FromError(executorResult);
+ return ResultExtensions.FromError(executorResult);
}
var data = await _guildData.GetData(guildId, CancellationToken);
@@ -111,13 +112,15 @@ public class MuteCommandGroup : CommandGroup
{
var failedEmbed = new EmbedBuilder()
.WithSmallTitle(Messages.InvalidTimeSpan, bot)
+ .WithDescription(Messages.TimeSpanExample)
.WithColour(ColorsList.Red)
.Build();
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken);
}
- return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, CancellationToken);
+ return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot,
+ CancellationToken);
}
private async Task MuteUserAsync(
@@ -125,11 +128,11 @@ public class MuteCommandGroup : CommandGroup
Snowflake channelId, IUser bot, CancellationToken ct = default)
{
var interactionResult
- = await _utility.CheckInteractionsAsync(
+ = await _access.CheckInteractionsAsync(
guildId, executor.ID, target.ID, "Mute", ct);
if (!interactionResult.IsSuccess)
{
- return Result.FromError(interactionResult);
+ return ResultExtensions.FromError(interactionResult);
}
if (interactionResult.Entity is not null)
@@ -142,14 +145,16 @@ public class MuteCommandGroup : CommandGroup
var until = DateTimeOffset.UtcNow.Add(duration); // >:)
- var muteMethodResult = await SelectMuteMethodAsync(executor, target, reason, duration, guildId, data, bot, until, ct);
+ var muteMethodResult =
+ await SelectMuteMethodAsync(executor, target, reason, duration, guildId, data, bot, until, ct);
if (!muteMethodResult.IsSuccess)
{
- return muteMethodResult;
+ return ResultExtensions.FromError(muteMethodResult);
}
var title = string.Format(Messages.UserMuted, target.GetTag());
- var description = new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason))
+ var description = new StringBuilder()
+ .AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason))
.AppendBulletPoint(string.Format(
Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString();
@@ -165,7 +170,7 @@ public class MuteCommandGroup : CommandGroup
private async Task SelectMuteMethodAsync(
IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data,
- IUser bot, DateTimeOffset until, CancellationToken ct)
+ IUser bot, DateTimeOffset until, CancellationToken ct = default)
{
var muteRole = GuildSettings.MuteRole.Get(data.Settings);
@@ -181,7 +186,7 @@ public class MuteCommandGroup : CommandGroup
private async Task RoleMuteUserAsync(
IUser executor, IUser target, string reason, Snowflake guildId, GuildData data,
- DateTimeOffset until, Snowflake muteRole, CancellationToken ct)
+ DateTimeOffset until, Snowflake muteRole, CancellationToken ct = default)
{
var assignRoles = new List { muteRole };
var memberData = data.GetOrCreateMemberData(target.ID);
@@ -203,7 +208,7 @@ public class MuteCommandGroup : CommandGroup
private async Task TimeoutUserAsync(
IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId,
- IUser bot, DateTimeOffset until, CancellationToken ct)
+ IUser bot, DateTimeOffset until, CancellationToken ct = default)
{
if (duration.TotalDays >= 28)
{
@@ -235,10 +240,10 @@ public class MuteCommandGroup : CommandGroup
///
///
[Command("unmute", "размут")]
- [DiscordDefaultMemberPermissions(DiscordPermission.ModerateMembers)]
+ [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
[DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)]
- [RequireDiscordPermission(DiscordPermission.ModerateMembers)]
+ [RequireDiscordPermission(DiscordPermission.ManageMessages)]
[RequireBotDiscordPermissions(DiscordPermission.ModerateMembers)]
[Description("Unmute member")]
[UsedImplicitly]
@@ -256,14 +261,14 @@ public class MuteCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
// Needed to get the tag and avatar
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor))
{
- return Result.FromError(executorResult);
+ return ResultExtensions.FromError(executorResult);
}
var data = await _guildData.GetData(guildId, CancellationToken);
@@ -286,11 +291,11 @@ public class MuteCommandGroup : CommandGroup
IUser bot, CancellationToken ct = default)
{
var interactionResult
- = await _utility.CheckInteractionsAsync(
+ = await _access.CheckInteractionsAsync(
guildId, executor.ID, target.ID, "Unmute", ct);
if (!interactionResult.IsSuccess)
{
- return Result.FromError(interactionResult);
+ return ResultExtensions.FromError(interactionResult);
}
if (interactionResult.Entity is not null)
@@ -323,14 +328,14 @@ public class MuteCommandGroup : CommandGroup
await RemoveMuteRoleAsync(executor, target, reason, guildId, memberData, CancellationToken);
if (!removeMuteRoleAsync.IsSuccess)
{
- return Result.FromError(removeMuteRoleAsync.Error);
+ return ResultExtensions.FromError(removeMuteRoleAsync);
}
var removeTimeoutResult =
await RemoveTimeoutAsync(executor, target, reason, guildId, communicationDisabledUntil, CancellationToken);
if (!removeTimeoutResult.IsSuccess)
{
- return Result.FromError(removeTimeoutResult.Error);
+ return ResultExtensions.FromError(removeTimeoutResult);
}
var title = string.Format(Messages.UserUnmuted, target.GetTag());
@@ -347,11 +352,12 @@ public class MuteCommandGroup : CommandGroup
}
private async Task RemoveMuteRoleAsync(
- IUser executor, IUser target, string reason, Snowflake guildId, MemberData memberData, CancellationToken ct = default)
+ IUser executor, IUser target, string reason, Snowflake guildId, MemberData memberData,
+ CancellationToken ct = default)
{
if (memberData.MutedUntil is null)
{
- return Result.FromSuccess();
+ return Result.Success;
}
var unmuteResult = await _guildApi.ModifyGuildMemberAsync(
@@ -371,7 +377,7 @@ public class MuteCommandGroup : CommandGroup
{
if (communicationDisabledUntil is null)
{
- return Result.FromSuccess();
+ return Result.Success;
}
var unmuteResult = await _guildApi.ModifyGuildMemberAsync(
diff --git a/src/Commands/PingCommandGroup.cs b/TeamOctolings.Octobot/Commands/PingCommandGroup.cs
similarity index 89%
rename from src/Commands/PingCommandGroup.cs
rename to TeamOctolings.Octobot/Commands/PingCommandGroup.cs
index 31fa6dc..01a1ee2 100644
--- a/src/Commands/PingCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/PingCommandGroup.cs
@@ -1,8 +1,5 @@
using System.ComponentModel;
using JetBrains.Annotations;
-using Octobot.Data;
-using Octobot.Extensions;
-using Octobot.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
@@ -15,14 +12,17 @@ using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Gateway;
using Remora.Rest.Core;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Services;
-namespace Octobot.Commands;
+namespace TeamOctolings.Octobot.Commands;
///
/// Handles the command to get the time taken for the gateway to respond to the last heartbeat: /ping
///
[UsedImplicitly]
-public class PingCommandGroup : CommandGroup
+public sealed class PingCommandGroup : CommandGroup
{
private readonly IDiscordRestChannelAPI _channelApi;
private readonly DiscordGatewayClient _client;
@@ -64,7 +64,7 @@ public class PingCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var cfg = await _guildData.GetSettings(guildId, CancellationToken);
@@ -84,14 +84,14 @@ public class PingCommandGroup : CommandGroup
channelId, limit: 1, ct: ct);
if (!lastMessageResult.IsDefined(out var lastMessage))
{
- return Result.FromError(lastMessageResult);
+ return ResultExtensions.FromError(lastMessageResult);
}
latency = DateTimeOffset.UtcNow.Subtract(lastMessage.Single().Timestamp).TotalMilliseconds;
}
var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot)
- .WithTitle($"Sound{Random.Shared.Next(1, 4)}".Localized())
+ .WithTitle($"Generic{Random.Shared.Next(1, 4)}".Localized())
.WithDescription($"{latency:F0}{Messages.Milliseconds}")
.WithColour(latency < 250 ? ColorsList.Green : latency < 500 ? ColorsList.Yellow : ColorsList.Red)
.WithCurrentTimestamp()
diff --git a/src/Commands/RemindCommandGroup.cs b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs
similarity index 92%
rename from src/Commands/RemindCommandGroup.cs
rename to TeamOctolings.Octobot/Commands/RemindCommandGroup.cs
index c270f30..3188d27 100644
--- a/src/Commands/RemindCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs
@@ -2,9 +2,6 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Text;
using JetBrains.Annotations;
-using Octobot.Data;
-using Octobot.Extensions;
-using Octobot.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
@@ -17,21 +14,24 @@ using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
-using Octobot.Parsers;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Parsers;
+using TeamOctolings.Octobot.Services;
-namespace Octobot.Commands;
+namespace TeamOctolings.Octobot.Commands;
///
/// Handles commands to manage reminders: /remind, /listremind, /delremind
///
[UsedImplicitly]
-public class RemindCommandGroup : CommandGroup
+public sealed class RemindCommandGroup : CommandGroup
{
private readonly IInteractionCommandContext _context;
private readonly IFeedbackService _feedback;
private readonly GuildDataService _guildData;
- private readonly IDiscordRestUserAPI _userApi;
private readonly IDiscordRestInteractionAPI _interactionApi;
+ private readonly IDiscordRestUserAPI _userApi;
public RemindCommandGroup(
IInteractionCommandContext context, GuildDataService guildData, IFeedbackService feedback,
@@ -63,13 +63,13 @@ public class RemindCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor))
{
- return Result.FromError(executorResult);
+ return ResultExtensions.FromError(executorResult);
}
var data = await _guildData.GetData(guildId, CancellationToken);
@@ -78,7 +78,7 @@ public class RemindCommandGroup : CommandGroup
return await ListRemindersAsync(data.GetOrCreateMemberData(executorId), guildId, executor, bot, CancellationToken);
}
- private Task ListRemindersAsync(MemberData data, Snowflake guildId, IUser executor, IUser bot, CancellationToken ct)
+ private Task ListRemindersAsync(MemberData data, Snowflake guildId, IUser executor, IUser bot, CancellationToken ct = default)
{
if (data.Reminders.Count == 0)
{
@@ -94,7 +94,7 @@ public class RemindCommandGroup : CommandGroup
{
var reminder = data.Reminders[i];
builder.AppendBulletPointLine(string.Format(Messages.ReminderPosition, Markdown.InlineCode((i + 1).ToString())))
- .AppendSubBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text)))
+ .AppendSubBulletPointLine(string.Format(Messages.ReminderText, reminder.Text))
.AppendSubBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At)))
.AppendSubBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}"));
}
@@ -120,7 +120,7 @@ public class RemindCommandGroup : CommandGroup
[RequireContext(ChannelContext.Guild)]
[UsedImplicitly]
public async Task ExecuteReminderAsync(
- [Description("After what period of time mention the reminder")]
+ [Description("After what period of time mention the reminder (e.g. 1h30m)")]
[Option("in")]
string timeSpanString,
[Description("Reminder text")] [MaxLength(512)]
@@ -134,13 +134,13 @@ public class RemindCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor))
{
- return Result.FromError(executorResult);
+ return ResultExtensions.FromError(executorResult);
}
var data = await _guildData.GetData(guildId, CancellationToken);
@@ -151,6 +151,7 @@ public class RemindCommandGroup : CommandGroup
{
var failedEmbed = new EmbedBuilder()
.WithSmallTitle(Messages.InvalidTimeSpan, bot)
+ .WithDescription(Messages.TimeSpanExample)
.WithColour(ColorsList.Red)
.Build();
@@ -181,7 +182,7 @@ public class RemindCommandGroup : CommandGroup
});
var builder = new StringBuilder()
- .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(text)))
+ .AppendLine(MarkdownExtensions.Quote(text))
.AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt)));
var embed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.ReminderCreated, executor.GetTag()), executor)
@@ -225,13 +226,13 @@ public class RemindCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor))
{
- return Result.FromError(executorResult);
+ return ResultExtensions.FromError(executorResult);
}
var data = await _guildData.GetData(guildId, CancellationToken);
@@ -264,6 +265,7 @@ public class RemindCommandGroup : CommandGroup
{
var failedEmbed = new EmbedBuilder()
.WithSmallTitle(Messages.InvalidTimeSpan, bot)
+ .WithDescription(Messages.TimeSpanExample)
.WithColour(ColorsList.Red)
.Build();
@@ -277,7 +279,7 @@ public class RemindCommandGroup : CommandGroup
data.Reminders.RemoveAt(index);
var builder = new StringBuilder()
- .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(oldReminder.Text)))
+ .AppendLine(MarkdownExtensions.Quote(oldReminder.Text))
.AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt)));
var embed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.ReminderEdited, executor.GetTag()), executor)
@@ -307,7 +309,7 @@ public class RemindCommandGroup : CommandGroup
data.Reminders.RemoveAt(index);
var builder = new StringBuilder()
- .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(value)))
+ .AppendLine(MarkdownExtensions.Quote(value))
.AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(oldReminder.At)));
var embed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.ReminderEdited, executor.GetTag()), executor)
@@ -341,7 +343,7 @@ public class RemindCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var data = await _guildData.GetData(guildId, CancellationToken);
@@ -351,7 +353,7 @@ public class RemindCommandGroup : CommandGroup
}
private Task DeleteReminderAsync(MemberData data, int index, IUser bot,
- CancellationToken ct)
+ CancellationToken ct = default)
{
if (index >= data.Reminders.Count)
{
@@ -365,7 +367,7 @@ public class RemindCommandGroup : CommandGroup
var reminder = data.Reminders[index];
var description = new StringBuilder()
- .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text)))
+ .AppendLine(MarkdownExtensions.Quote(reminder.Text))
.AppendBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At)));
data.Reminders.RemoveAt(index);
diff --git a/src/Commands/SettingsCommandGroup.cs b/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs
similarity index 85%
rename from src/Commands/SettingsCommandGroup.cs
rename to TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs
index acfb8ed..15aa42b 100644
--- a/src/Commands/SettingsCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs
@@ -3,10 +3,6 @@ using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Text.Json.Nodes;
using JetBrains.Annotations;
-using Octobot.Data;
-using Octobot.Data.Options;
-using Octobot.Extensions;
-using Octobot.Services;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
@@ -19,26 +15,31 @@ using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Data.Options;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Services;
-namespace Octobot.Commands;
+namespace TeamOctolings.Octobot.Commands;
///
/// Handles the commands to list and modify per-guild settings: /settings and /settings list.
///
[UsedImplicitly]
-public class SettingsCommandGroup : CommandGroup
+public sealed class SettingsCommandGroup : CommandGroup
{
///
- /// Represents all options as an array of objects implementing .
+ /// Represents all options as an array of objects implementing .
///
///
/// WARNING: If you update this array in any way, you must also update and make sure
/// that the orders match.
///
- private static readonly IOption[] AllOptions =
+ private static readonly IGuildOption[] AllOptions =
[
GuildSettings.Language,
GuildSettings.WelcomeMessage,
+ GuildSettings.LeaveMessage,
GuildSettings.ReceiveStartupMessages,
GuildSettings.RemoveRolesOnMute,
GuildSettings.ReturnRolesOnRejoin,
@@ -46,9 +47,11 @@ public class SettingsCommandGroup : CommandGroup
GuildSettings.RenameHoistedUsers,
GuildSettings.PublicFeedbackChannel,
GuildSettings.PrivateFeedbackChannel,
+ GuildSettings.WelcomeMessagesChannel,
GuildSettings.EventNotificationChannel,
GuildSettings.DefaultRole,
GuildSettings.MuteRole,
+ GuildSettings.ModeratorRole,
GuildSettings.EventNotificationRole,
GuildSettings.EventEarlyNotificationOffset
];
@@ -96,7 +99,7 @@ public class SettingsCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var cfg = await _guildData.GetSettings(guildId, CancellationToken);
@@ -179,13 +182,13 @@ public class SettingsCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
if (!executorResult.IsDefined(out var executor))
{
- return Result.FromError(executorResult);
+ return ResultExtensions.FromError(executorResult);
}
var data = await _guildData.GetData(guildId, CancellationToken);
@@ -196,9 +199,30 @@ public class SettingsCommandGroup : CommandGroup
}
private async Task EditSettingAsync(
- IOption option, string value, GuildData data, Snowflake channelId, IUser executor, IUser bot,
+ IGuildOption option, string value, GuildData data, Snowflake channelId, IUser executor, IUser bot,
CancellationToken ct = default)
{
+ var equalsResult = option.ValueEquals(data.Settings, value);
+ if (!equalsResult.IsSuccess)
+ {
+ var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, bot)
+ .WithDescription(equalsResult.Error.Message)
+ .WithColour(ColorsList.Red)
+ .Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
+ }
+
+ if (equalsResult.Entity)
+ {
+ var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, bot)
+ .WithDescription(Messages.SettingValueEquals)
+ .WithColour(ColorsList.Red)
+ .Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
+ }
+
var setResult = option.Set(data.Settings, value);
if (!setResult.IsSuccess)
{
@@ -239,7 +263,7 @@ public class SettingsCommandGroup : CommandGroup
[DiscordDefaultDMPermission(false)]
[RequireContext(ChannelContext.Guild)]
[RequireDiscordPermission(DiscordPermission.ManageGuild)]
- [Description("Reset settings for this server")]
+ [Description("Reset settings for this guild")]
[UsedImplicitly]
public async Task ExecuteResetSettingsAsync(
[Description("Setting to reset")] AllOptionsEnum? setting = null)
@@ -252,7 +276,7 @@ public class SettingsCommandGroup : CommandGroup
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
var cfg = await _guildData.GetSettings(guildId, CancellationToken);
@@ -267,12 +291,12 @@ public class SettingsCommandGroup : CommandGroup
}
private async Task ResetSingleSettingAsync(JsonNode cfg, IUser bot,
- IOption option, CancellationToken ct = default)
+ IGuildOption option, CancellationToken ct = default)
{
var resetResult = option.Reset(cfg);
if (!resetResult.IsSuccess)
{
- return Result.FromError(resetResult.Error);
+ return ResultExtensions.FromError(resetResult);
}
var embed = new EmbedBuilder().WithSmallTitle(
diff --git a/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs
new file mode 100644
index 0000000..2936392
--- /dev/null
+++ b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs
@@ -0,0 +1,272 @@
+using System.ComponentModel;
+using System.Text;
+using JetBrains.Annotations;
+using Remora.Commands.Attributes;
+using Remora.Commands.Groups;
+using Remora.Discord.API.Abstractions.Objects;
+using Remora.Discord.API.Abstractions.Rest;
+using Remora.Discord.Commands.Attributes;
+using Remora.Discord.Commands.Contexts;
+using Remora.Discord.Commands.Feedback.Services;
+using Remora.Discord.Extensions.Embeds;
+using Remora.Discord.Extensions.Formatting;
+using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Parsers;
+using TeamOctolings.Octobot.Services;
+
+namespace TeamOctolings.Octobot.Commands;
+
+///
+/// Handles tool commands: /random, /timestamp, /8ball.
+///
+[UsedImplicitly]
+public sealed class ToolsCommandGroup : CommandGroup
+{
+ private static readonly TimestampStyle[] AllStyles =
+ [
+ TimestampStyle.ShortDate,
+ TimestampStyle.LongDate,
+ TimestampStyle.ShortTime,
+ TimestampStyle.LongTime,
+ TimestampStyle.ShortDateTime,
+ TimestampStyle.LongDateTime,
+ TimestampStyle.RelativeTime
+ ];
+
+ private static readonly string[] AnswerTypes =
+ [
+ "Positive", "Questionable", "Neutral", "Negative"
+ ];
+
+ private readonly ICommandContext _context;
+ private readonly IFeedbackService _feedback;
+ private readonly GuildDataService _guildData;
+ private readonly IDiscordRestUserAPI _userApi;
+
+ public ToolsCommandGroup(
+ ICommandContext context, IFeedbackService feedback,
+ GuildDataService guildData, IDiscordRestUserAPI userApi)
+ {
+ _context = context;
+ _guildData = guildData;
+ _feedback = feedback;
+ _userApi = userApi;
+ }
+
+ ///
+ /// A slash command that generates a random number using maximum and minimum numbers.
+ ///
+ /// The first number used for randomization.
+ /// The second number used for randomization. Default value: 0
+ ///
+ /// A feedback sending result which may or may not have succeeded.
+ ///
+ [Command("random")]
+ [DiscordDefaultDMPermission(false)]
+ [Description("Generates a random number")]
+ [UsedImplicitly]
+ public async Task ExecuteRandomAsync(
+ [Description("First number")] long first,
+ [Description("Second number (Default: 0)")]
+ long? second = null)
+ {
+ if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId))
+ {
+ return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
+ }
+
+ var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
+ if (!executorResult.IsDefined(out var executor))
+ {
+ return ResultExtensions.FromError(executorResult);
+ }
+
+ var data = await _guildData.GetData(guildId, CancellationToken);
+ Messages.Culture = GuildSettings.Language.Get(data.Settings);
+
+ return await SendRandomNumberAsync(first, second, executor, CancellationToken);
+ }
+
+ private Task SendRandomNumberAsync(long first, long? secondNullable,
+ IUser executor, CancellationToken ct = default)
+ {
+ const long secondDefault = 0;
+ var second = secondNullable ?? secondDefault;
+
+ var min = Math.Min(first, second);
+ var max = Math.Max(first, second);
+
+ var i = Random.Shared.NextInt64(min, max + 1);
+
+ var description = new StringBuilder().Append("# ").Append(i);
+
+ description.AppendLine().AppendBulletPoint(string.Format(
+ Messages.RandomMin, Markdown.InlineCode(min.ToString())));
+ if (secondNullable is null && first >= secondDefault)
+ {
+ description.Append(' ').Append(Messages.Default);
+ }
+
+ description.AppendLine().AppendBulletPoint(string.Format(
+ Messages.RandomMax, Markdown.InlineCode(max.ToString())));
+ if (secondNullable is null && first < secondDefault)
+ {
+ description.Append(' ').Append(Messages.Default);
+ }
+
+ var embedColor = ColorsList.Blue;
+ if (secondNullable is not null && min == max)
+ {
+ description.AppendLine().Append(Markdown.Italicise(Messages.RandomMinMaxSame));
+ embedColor = ColorsList.Red;
+ }
+
+ var embed = new EmbedBuilder().WithSmallTitle(
+ string.Format(Messages.RandomTitle, executor.GetTag()), executor)
+ .WithDescription(description.ToString())
+ .WithColour(embedColor)
+ .Build();
+
+ return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
+ }
+
+ ///
+ /// A slash command that shows the current timestamp with an optional offset in all styles supported by Discord.
+ ///
+ /// The offset for the current timestamp.
+ ///
+ /// A feedback sending result which may or may not have succeeded.
+ ///
+ [Command("timestamp")]
+ [DiscordDefaultDMPermission(false)]
+ [Description("Shows a timestamp in all styles")]
+ [UsedImplicitly]
+ public async Task ExecuteTimestampAsync(
+ [Description("Offset from current time")] [Option("offset")]
+ string? stringOffset = null)
+ {
+ if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId))
+ {
+ return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
+ }
+
+ var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
+ if (!botResult.IsDefined(out var bot))
+ {
+ return ResultExtensions.FromError(botResult);
+ }
+
+ var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
+ if (!executorResult.IsDefined(out var executor))
+ {
+ return ResultExtensions.FromError(executorResult);
+ }
+
+ var data = await _guildData.GetData(guildId, CancellationToken);
+ Messages.Culture = GuildSettings.Language.Get(data.Settings);
+
+ if (stringOffset is null)
+ {
+ return await SendTimestampAsync(null, executor, CancellationToken);
+ }
+
+ var parseResult = TimeSpanParser.TryParse(stringOffset);
+ if (!parseResult.IsDefined(out var offset))
+ {
+ var failedEmbed = new EmbedBuilder()
+ .WithSmallTitle(Messages.InvalidTimeSpan, bot)
+ .WithDescription(Messages.TimeSpanExample)
+ .WithColour(ColorsList.Red)
+ .Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken);
+ }
+
+ return await SendTimestampAsync(offset, executor, CancellationToken);
+ }
+
+ private Task SendTimestampAsync(TimeSpan? offset, IUser executor, CancellationToken ct = default)
+ {
+ var timestamp = DateTimeOffset.UtcNow.Add(offset ?? TimeSpan.Zero).ToUnixTimeSeconds();
+
+ var description = new StringBuilder().Append("# ").AppendLine(timestamp.ToString());
+
+ if (offset is not null)
+ {
+ description.AppendLine(string.Format(
+ Messages.TimestampOffset, Markdown.InlineCode(offset.ToString() ?? string.Empty))).AppendLine();
+ }
+
+ foreach (var markdownTimestamp in AllStyles.Select(style => Markdown.Timestamp(timestamp, style)))
+ {
+ description.AppendBulletPoint(Markdown.InlineCode(markdownTimestamp))
+ .Append(" → ").AppendLine(markdownTimestamp);
+ }
+
+ var embed = new EmbedBuilder().WithSmallTitle(
+ string.Format(Messages.TimestampTitle, executor.GetTag()), executor)
+ .WithDescription(description.ToString())
+ .WithColour(ColorsList.Blue)
+ .Build();
+
+ return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
+ }
+
+ ///
+ /// A slash command that shows a random answer from the Magic 8-Ball.
+ ///
+ /// Unused input.
+ ///
+ /// The 8-Ball answers were taken from Wikipedia.
+ ///
+ ///
+ /// A feedback sending result which may or may not have succeeded.
+ ///
+ [Command("8ball")]
+ [DiscordDefaultDMPermission(false)]
+ [Description("Ask the Magic 8-Ball a question")]
+ [UsedImplicitly]
+ public async Task ExecuteEightBallAsync(
+ // let the user think he's actually asking the ball a question
+ [Description("Question to ask")] string question)
+ {
+ if (!_context.TryGetContextIDs(out var guildId, out _, out _))
+ {
+ return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
+ }
+
+ var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
+ if (!botResult.IsDefined(out var bot))
+ {
+ return ResultExtensions.FromError(botResult);
+ }
+
+ var data = await _guildData.GetData(guildId, CancellationToken);
+ Messages.Culture = GuildSettings.Language.Get(data.Settings);
+
+ return await AnswerEightBallAsync(bot, CancellationToken);
+ }
+
+ private Task AnswerEightBallAsync(IUser bot, CancellationToken ct = default)
+ {
+ var typeNumber = Random.Shared.Next(0, 4);
+ var embedColor = typeNumber switch
+ {
+ 0 => ColorsList.Blue,
+ 1 => ColorsList.Green,
+ 2 => ColorsList.Yellow,
+ 3 => ColorsList.Red,
+ _ => throw new ArgumentOutOfRangeException(null, nameof(typeNumber))
+ };
+
+ var answer = $"EightBall{AnswerTypes[typeNumber]}{Random.Shared.Next(1, 6)}".Localized();
+
+ var embed = new EmbedBuilder().WithSmallTitle(answer, bot)
+ .WithColour(embedColor)
+ .Build();
+
+ return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
+ }
+}
diff --git a/src/Data/GuildData.cs b/TeamOctolings.Octobot/Data/GuildData.cs
similarity index 97%
rename from src/Data/GuildData.cs
rename to TeamOctolings.Octobot/Data/GuildData.cs
index 5a903d6..f393323 100644
--- a/src/Data/GuildData.cs
+++ b/TeamOctolings.Octobot/Data/GuildData.cs
@@ -1,7 +1,7 @@
using System.Text.Json.Nodes;
using Remora.Rest.Core;
-namespace Octobot.Data;
+namespace TeamOctolings.Octobot.Data;
///
/// Stores information about a guild. This information is not accessible via the Discord API.
diff --git a/src/Data/GuildSettings.cs b/TeamOctolings.Octobot/Data/GuildSettings.cs
similarity index 70%
rename from src/Data/GuildSettings.cs
rename to TeamOctolings.Octobot/Data/GuildSettings.cs
index cdaede6..dc59d6f 100644
--- a/src/Data/GuildSettings.cs
+++ b/TeamOctolings.Octobot/Data/GuildSettings.cs
@@ -1,8 +1,8 @@
-using Octobot.Data.Options;
-using Octobot.Responders;
using Remora.Discord.API.Abstractions.Objects;
+using TeamOctolings.Octobot.Data.Options;
+using TeamOctolings.Octobot.Responders;
-namespace Octobot.Data;
+namespace TeamOctolings.Octobot.Data;
///
/// Contains all per-guild settings that can be set by a member
@@ -13,16 +13,28 @@ public static class GuildSettings
public static readonly LanguageOption Language = new("Language", "en");
///
- /// Controls what message should be sent in when a new member joins the server.
+ /// Controls what message should be sent in when a new member joins the guild.
///
///
///
/// - No message will be sent if set to "off", "disable" or "disabled".
- /// - will be sent if set to "default" or "reset"
+ /// - will be sent if set to "default" or "reset".
///
///
///
- public static readonly Option WelcomeMessage = new("WelcomeMessage", "default");
+ public static readonly GuildOption WelcomeMessage = new("WelcomeMessage", "default");
+
+ ///
+ /// Controls what message should be sent in when a member leaves the guild.
+ ///
+ ///
+ ///
+ /// - No message will be sent if set to "off", "disable" or "disabled".
+ /// - will be sent if set to "default" or "reset".
+ ///
+ ///
+ ///
+ public static readonly GuildOption LeaveMessage = new("LeaveMessage", "default");
///
/// Controls whether or not the message should be sent
@@ -56,9 +68,15 @@ public static class GuildSettings
///
public static readonly SnowflakeOption PrivateFeedbackChannel = new("PrivateFeedbackChannel");
+ ///
+ /// Controls what channel should welcome messages be sent to.
+ ///
+ public static readonly SnowflakeOption WelcomeMessagesChannel = new("WelcomeMessagesChannel");
+
public static readonly SnowflakeOption EventNotificationChannel = new("EventNotificationChannel");
public static readonly SnowflakeOption DefaultRole = new("DefaultRole");
public static readonly SnowflakeOption MuteRole = new("MuteRole");
+ public static readonly SnowflakeOption ModeratorRole = new("ModeratorRole");
public static readonly SnowflakeOption EventNotificationRole = new("EventNotificationRole");
///
diff --git a/src/Data/MemberData.cs b/TeamOctolings.Octobot/Data/MemberData.cs
similarity index 75%
rename from src/Data/MemberData.cs
rename to TeamOctolings.Octobot/Data/MemberData.cs
index 8e23e54..984d4af 100644
--- a/src/Data/MemberData.cs
+++ b/TeamOctolings.Octobot/Data/MemberData.cs
@@ -1,14 +1,13 @@
-namespace Octobot.Data;
+namespace TeamOctolings.Octobot.Data;
///
/// Stores information about a member
///
public sealed class MemberData
{
- public MemberData(ulong id, DateTimeOffset? bannedUntil = null, List? reminders = null)
+ public MemberData(ulong id, List? reminders = null)
{
Id = id;
- BannedUntil = bannedUntil;
if (reminders is not null)
{
Reminders = reminders;
diff --git a/src/Data/Options/AllOptionsEnum.cs b/TeamOctolings.Octobot/Data/Options/AllOptionsEnum.cs
similarity index 81%
rename from src/Data/Options/AllOptionsEnum.cs
rename to TeamOctolings.Octobot/Data/Options/AllOptionsEnum.cs
index a96a9ac..6a4280e 100644
--- a/src/Data/Options/AllOptionsEnum.cs
+++ b/TeamOctolings.Octobot/Data/Options/AllOptionsEnum.cs
@@ -1,7 +1,7 @@
using JetBrains.Annotations;
-using Octobot.Commands;
+using TeamOctolings.Octobot.Commands;
-namespace Octobot.Data.Options;
+namespace TeamOctolings.Octobot.Data.Options;
///
/// Represents all options as enums.
@@ -14,6 +14,7 @@ public enum AllOptionsEnum
{
[UsedImplicitly] Language,
[UsedImplicitly] WelcomeMessage,
+ [UsedImplicitly] LeaveMessage,
[UsedImplicitly] ReceiveStartupMessages,
[UsedImplicitly] RemoveRolesOnMute,
[UsedImplicitly] ReturnRolesOnRejoin,
@@ -21,9 +22,11 @@ public enum AllOptionsEnum
[UsedImplicitly] RenameHoistedUsers,
[UsedImplicitly] PublicFeedbackChannel,
[UsedImplicitly] PrivateFeedbackChannel,
+ [UsedImplicitly] WelcomeMessagesChannel,
[UsedImplicitly] EventNotificationChannel,
[UsedImplicitly] DefaultRole,
[UsedImplicitly] MuteRole,
+ [UsedImplicitly] ModeratorRole,
[UsedImplicitly] EventNotificationRole,
[UsedImplicitly] EventEarlyNotificationOffset
}
diff --git a/src/Data/Options/BoolOption.cs b/TeamOctolings.Octobot/Data/Options/BoolOption.cs
similarity index 69%
rename from src/Data/Options/BoolOption.cs
rename to TeamOctolings.Octobot/Data/Options/BoolOption.cs
index 130687e..3b81abb 100644
--- a/src/Data/Options/BoolOption.cs
+++ b/TeamOctolings.Octobot/Data/Options/BoolOption.cs
@@ -1,9 +1,9 @@
using System.Text.Json.Nodes;
using Remora.Results;
-namespace Octobot.Data.Options;
+namespace TeamOctolings.Octobot.Data.Options;
-public sealed class BoolOption : Option
+public sealed class BoolOption : GuildOption
{
public BoolOption(string name, bool defaultValue) : base(name, defaultValue) { }
@@ -12,6 +12,16 @@ public sealed class BoolOption : Option
return Get(settings) ? Messages.Yes : Messages.No;
}
+ public override Result ValueEquals(JsonNode settings, string value)
+ {
+ if (!TryParseBool(value, out var boolean))
+ {
+ return new ArgumentInvalidError(nameof(value), Messages.InvalidSettingValue);
+ }
+
+ return Value(settings).Equals(boolean.ToString());
+ }
+
public override Result Set(JsonNode settings, string from)
{
if (!TryParseBool(from, out var value))
@@ -20,7 +30,7 @@ public sealed class BoolOption : Option
}
settings[Name] = value;
- return Result.FromSuccess();
+ return Result.Success;
}
private static bool TryParseBool(string from, out bool value)
diff --git a/src/Data/Options/Option.cs b/TeamOctolings.Octobot/Data/Options/GuildOption.cs
similarity index 71%
rename from src/Data/Options/Option.cs
rename to TeamOctolings.Octobot/Data/Options/GuildOption.cs
index 0ba8ce1..ea9c30e 100644
--- a/src/Data/Options/Option.cs
+++ b/TeamOctolings.Octobot/Data/Options/GuildOption.cs
@@ -2,18 +2,18 @@ using System.Text.Json.Nodes;
using Remora.Discord.Extensions.Formatting;
using Remora.Results;
-namespace Octobot.Data.Options;
+namespace TeamOctolings.Octobot.Data.Options;
///
-/// Represents an per-guild option.
+/// Represents a per-guild option.
///
/// The type of the option.
-public class Option : IOption
+public class GuildOption : IGuildOption
where T : notnull
{
protected readonly T DefaultValue;
- public Option(string name, T defaultValue)
+ public GuildOption(string name, T defaultValue)
{
Name = name;
DefaultValue = defaultValue;
@@ -21,9 +21,19 @@ public class Option : IOption
public string Name { get; }
+ protected virtual string Value(JsonNode settings)
+ {
+ return Get(settings).ToString() ?? throw new InvalidOperationException();
+ }
+
public virtual string Display(JsonNode settings)
{
- return Markdown.InlineCode(Get(settings).ToString() ?? throw new InvalidOperationException());
+ return Markdown.InlineCode(Value(settings));
+ }
+
+ public virtual Result ValueEquals(JsonNode settings, string value)
+ {
+ return Value(settings).Equals(value);
}
///
@@ -35,7 +45,13 @@ public class Option : IOption
public virtual Result Set(JsonNode settings, string from)
{
settings[Name] = from;
- return Result.FromSuccess();
+ return Result.Success;
+ }
+
+ public Result Reset(JsonNode settings)
+ {
+ settings[Name] = null;
+ return Result.Success;
}
///
@@ -48,10 +64,4 @@ public class Option : IOption
var property = settings[Name];
return property != null ? property.GetValue() : DefaultValue;
}
-
- public Result Reset(JsonNode settings)
- {
- settings[Name] = null;
- return Result.FromSuccess();
- }
}
diff --git a/src/Data/Options/IOption.cs b/TeamOctolings.Octobot/Data/Options/IGuildOption.cs
similarity index 59%
rename from src/Data/Options/IOption.cs
rename to TeamOctolings.Octobot/Data/Options/IGuildOption.cs
index b8ed03c..9920281 100644
--- a/src/Data/Options/IOption.cs
+++ b/TeamOctolings.Octobot/Data/Options/IGuildOption.cs
@@ -1,12 +1,13 @@
using System.Text.Json.Nodes;
using Remora.Results;
-namespace Octobot.Data.Options;
+namespace TeamOctolings.Octobot.Data.Options;
-public interface IOption
+public interface IGuildOption
{
string Name { get; }
string Display(JsonNode settings);
+ Result ValueEquals(JsonNode settings, string value);
Result Set(JsonNode settings, string from);
Result Reset(JsonNode settings);
}
diff --git a/src/Data/Options/LanguageOption.cs b/TeamOctolings.Octobot/Data/Options/LanguageOption.cs
similarity index 71%
rename from src/Data/Options/LanguageOption.cs
rename to TeamOctolings.Octobot/Data/Options/LanguageOption.cs
index 464c61b..f58e011 100644
--- a/src/Data/Options/LanguageOption.cs
+++ b/TeamOctolings.Octobot/Data/Options/LanguageOption.cs
@@ -1,25 +1,23 @@
using System.Globalization;
using System.Text.Json.Nodes;
-using Remora.Discord.Extensions.Formatting;
using Remora.Results;
-namespace Octobot.Data.Options;
+namespace TeamOctolings.Octobot.Data.Options;
///
-public sealed class LanguageOption : Option
+public sealed class LanguageOption : GuildOption
{
private static readonly Dictionary CultureInfoCache = new()
{
{ "en", new CultureInfo("en-US") },
- { "ru", new CultureInfo("ru-RU") },
- { "mctaylors-ru", new CultureInfo("tt-RU") }
+ { "ru", new CultureInfo("ru-RU") }
};
public LanguageOption(string name, string defaultValue) : base(name, CultureInfoCache[defaultValue]) { }
- public override string Display(JsonNode settings)
+ protected override string Value(JsonNode settings)
{
- return Markdown.InlineCode(settings[Name]?.GetValue() ?? "en");
+ return settings[Name]?.GetValue() ?? "en";
}
///
diff --git a/src/Data/Options/SnowflakeOption.cs b/TeamOctolings.Octobot/Data/Options/SnowflakeOption.cs
similarity index 84%
rename from src/Data/Options/SnowflakeOption.cs
rename to TeamOctolings.Octobot/Data/Options/SnowflakeOption.cs
index 66ada96..b7405f2 100644
--- a/src/Data/Options/SnowflakeOption.cs
+++ b/TeamOctolings.Octobot/Data/Options/SnowflakeOption.cs
@@ -1,13 +1,13 @@
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
-using Octobot.Extensions;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
+using TeamOctolings.Octobot.Extensions;
-namespace Octobot.Data.Options;
+namespace TeamOctolings.Octobot.Data.Options;
-public sealed partial class SnowflakeOption : Option
+public sealed partial class SnowflakeOption : GuildOption
{
public SnowflakeOption(string name) : base(name, 0UL.ToSnowflake()) { }
@@ -32,7 +32,7 @@ public sealed partial class SnowflakeOption : Option
}
settings[Name] = parsed;
- return Result.FromSuccess();
+ return Result.Success;
}
[GeneratedRegex("[^0-9]")]
diff --git a/src/Data/Options/TimeSpanOption.cs b/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs
similarity index 56%
rename from src/Data/Options/TimeSpanOption.cs
rename to TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs
index c81a02d..7e21343 100644
--- a/src/Data/Options/TimeSpanOption.cs
+++ b/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs
@@ -1,13 +1,23 @@
using System.Text.Json.Nodes;
-using Octobot.Parsers;
using Remora.Results;
+using TeamOctolings.Octobot.Parsers;
-namespace Octobot.Data.Options;
+namespace TeamOctolings.Octobot.Data.Options;
-public sealed class TimeSpanOption : Option
+public sealed class TimeSpanOption : GuildOption
{
public TimeSpanOption(string name, TimeSpan defaultValue) : base(name, defaultValue) { }
+ public override Result ValueEquals(JsonNode settings, string value)
+ {
+ if (!TimeSpanParser.TryParse(value).IsDefined(out var span))
+ {
+ return new ArgumentInvalidError(nameof(value), Messages.InvalidSettingValue);
+ }
+
+ return Value(settings).Equals(span.ToString());
+ }
+
public override TimeSpan Get(JsonNode settings)
{
var property = settings[Name];
@@ -22,6 +32,6 @@ public sealed class TimeSpanOption : Option
}
settings[Name] = span.ToString();
- return Result.FromSuccess();
+ return Result.Success;
}
}
diff --git a/src/Data/Reminder.cs b/TeamOctolings.Octobot/Data/Reminder.cs
similarity index 83%
rename from src/Data/Reminder.cs
rename to TeamOctolings.Octobot/Data/Reminder.cs
index f21b222..c3936da 100644
--- a/src/Data/Reminder.cs
+++ b/TeamOctolings.Octobot/Data/Reminder.cs
@@ -1,4 +1,4 @@
-namespace Octobot.Data;
+namespace TeamOctolings.Octobot.Data;
public struct Reminder
{
diff --git a/src/Data/ScheduledEventData.cs b/TeamOctolings.Octobot/Data/ScheduledEventData.cs
similarity index 97%
rename from src/Data/ScheduledEventData.cs
rename to TeamOctolings.Octobot/Data/ScheduledEventData.cs
index 59efc63..7ba6e92 100644
--- a/src/Data/ScheduledEventData.cs
+++ b/TeamOctolings.Octobot/Data/ScheduledEventData.cs
@@ -1,7 +1,7 @@
using System.Text.Json.Serialization;
using Remora.Discord.API.Abstractions.Objects;
-namespace Octobot.Data;
+namespace TeamOctolings.Octobot.Data;
///
/// Stores information about scheduled events. This information is not provided by the Discord API.
diff --git a/src/Extensions/ChannelApiExtensions.cs b/TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs
similarity index 70%
rename from src/Extensions/ChannelApiExtensions.cs
rename to TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs
index 12ccf35..82f8889 100644
--- a/src/Extensions/ChannelApiExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs
@@ -5,25 +5,26 @@ using Remora.Discord.API.Objects;
using Remora.Rest.Core;
using Remora.Results;
-namespace Octobot.Extensions;
+namespace TeamOctolings.Octobot.Extensions;
public static class ChannelApiExtensions
{
public static async Task CreateMessageWithEmbedResultAsync(this IDiscordRestChannelAPI channelApi,
Snowflake channelId, Optional message = default, Optional nonce = default,
Optional isTextToSpeech = default, Optional> embedResult = default,
- Optional allowedMentions = default, Optional messageRefenence = default,
+ Optional allowedMentions = default, Optional messageReference = default,
Optional> components = default,
Optional> stickerIds = default,
Optional>> attachments = default,
- Optional flags = default, CancellationToken ct = default)
+ Optional flags = default, Optional enforceNonce = default,
+ Optional poll = default, CancellationToken ct = default)
{
if (!embedResult.IsDefined() || !embedResult.Value.IsDefined(out var embed))
{
- return Result.FromError(embedResult.Value);
+ return ResultExtensions.FromError(embedResult.Value);
}
return (Result)await channelApi.CreateMessageAsync(channelId, message, nonce, isTextToSpeech, new[] { embed },
- allowedMentions, messageRefenence, components, stickerIds, attachments, flags, ct);
+ allowedMentions, messageReference, components, stickerIds, attachments, flags, enforceNonce, poll, ct);
}
}
diff --git a/src/Extensions/CollectionExtensions.cs b/TeamOctolings.Octobot/Extensions/CollectionExtensions.cs
similarity index 93%
rename from src/Extensions/CollectionExtensions.cs
rename to TeamOctolings.Octobot/Extensions/CollectionExtensions.cs
index 9c873f2..3ea13a8 100644
--- a/src/Extensions/CollectionExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/CollectionExtensions.cs
@@ -1,6 +1,6 @@
using Remora.Results;
-namespace Octobot.Extensions;
+namespace TeamOctolings.Octobot.Extensions;
public static class CollectionExtensions
{
@@ -32,7 +32,7 @@ public static class CollectionExtensions
{
return list.Count switch
{
- 0 => Result.FromSuccess(),
+ 0 => Result.Success,
1 => list[0],
_ => new AggregateError(list.Cast().ToArray())
};
diff --git a/src/Extensions/CommandContextExtensions.cs b/TeamOctolings.Octobot/Extensions/CommandContextExtensions.cs
similarity index 92%
rename from src/Extensions/CommandContextExtensions.cs
rename to TeamOctolings.Octobot/Extensions/CommandContextExtensions.cs
index a0c02f2..16b8b56 100644
--- a/src/Extensions/CommandContextExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/CommandContextExtensions.cs
@@ -2,7 +2,7 @@
using Remora.Discord.Commands.Extensions;
using Remora.Rest.Core;
-namespace Octobot.Extensions;
+namespace TeamOctolings.Octobot.Extensions;
public static class CommandContextExtensions
{
diff --git a/src/Extensions/DiffPaneModelExtensions.cs b/TeamOctolings.Octobot/Extensions/DiffPaneModelExtensions.cs
similarity index 94%
rename from src/Extensions/DiffPaneModelExtensions.cs
rename to TeamOctolings.Octobot/Extensions/DiffPaneModelExtensions.cs
index 1c3a098..3bb707b 100644
--- a/src/Extensions/DiffPaneModelExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/DiffPaneModelExtensions.cs
@@ -1,7 +1,7 @@
using System.Text;
using DiffPlex.DiffBuilder.Model;
-namespace Octobot.Extensions;
+namespace TeamOctolings.Octobot.Extensions;
public static class DiffPaneModelExtensions
{
diff --git a/src/Extensions/EmbedBuilderExtensions.cs b/TeamOctolings.Octobot/Extensions/EmbedBuilderExtensions.cs
similarity index 99%
rename from src/Extensions/EmbedBuilderExtensions.cs
rename to TeamOctolings.Octobot/Extensions/EmbedBuilderExtensions.cs
index 2d61403..dab0265 100644
--- a/src/Extensions/EmbedBuilderExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/EmbedBuilderExtensions.cs
@@ -4,7 +4,7 @@ using Remora.Discord.API.Objects;
using Remora.Discord.Extensions.Embeds;
using Remora.Rest.Core;
-namespace Octobot.Extensions;
+namespace TeamOctolings.Octobot.Extensions;
public static class EmbedBuilderExtensions
{
diff --git a/src/Extensions/FeedbackServiceExtensions.cs b/TeamOctolings.Octobot/Extensions/FeedbackServiceExtensions.cs
similarity index 85%
rename from src/Extensions/FeedbackServiceExtensions.cs
rename to TeamOctolings.Octobot/Extensions/FeedbackServiceExtensions.cs
index 40e0d53..c66c946 100644
--- a/src/Extensions/FeedbackServiceExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/FeedbackServiceExtensions.cs
@@ -3,7 +3,7 @@ using Remora.Discord.Commands.Feedback.Messages;
using Remora.Discord.Commands.Feedback.Services;
using Remora.Results;
-namespace Octobot.Extensions;
+namespace TeamOctolings.Octobot.Extensions;
public static class FeedbackServiceExtensions
{
@@ -13,7 +13,7 @@ public static class FeedbackServiceExtensions
{
if (!embedResult.IsDefined(out var embed))
{
- return Result.FromError(embedResult);
+ return ResultExtensions.FromError(embedResult);
}
return (Result)await feedback.SendContextualEmbedAsync(embed, options, ct);
diff --git a/src/Extensions/GuildScheduledEventExtensions.cs b/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs
similarity index 92%
rename from src/Extensions/GuildScheduledEventExtensions.cs
rename to TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs
index e3217e3..b8eb2d1 100644
--- a/src/Extensions/GuildScheduledEventExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs
@@ -2,7 +2,7 @@
using Remora.Rest.Core;
using Remora.Results;
-namespace Octobot.Extensions;
+namespace TeamOctolings.Octobot.Extensions;
public static class GuildScheduledEventExtensions
{
@@ -22,7 +22,7 @@ public static class GuildScheduledEventExtensions
}
return scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out endTime)
- ? Result.FromSuccess()
+ ? Result.Success
: new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime));
}
}
diff --git a/src/Extensions/LoggerExtensions.cs b/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs
similarity index 86%
rename from src/Extensions/LoggerExtensions.cs
rename to TeamOctolings.Octobot/Extensions/LoggerExtensions.cs
index 9df90b8..fac4dda 100644
--- a/src/Extensions/LoggerExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs
@@ -1,8 +1,7 @@
using Microsoft.Extensions.Logging;
-using Remora.Discord.Commands.Extensions;
using Remora.Results;
-namespace Octobot.Extensions;
+namespace TeamOctolings.Octobot.Extensions;
public static class LoggerExtensions
{
@@ -19,14 +18,14 @@ public static class LoggerExtensions
/// The message to use if this result has failed.
public static void LogResult(this ILogger logger, IResult result, string? message = "")
{
- if (result.IsSuccess || result.Error.IsUserOrEnvironmentError())
+ if (result.IsSuccess)
{
return;
}
if (result.Error is ExceptionError exe)
{
- if (exe.Exception is TaskCanceledException)
+ if (exe.Exception is OperationCanceledException)
{
return;
}
diff --git a/src/Extensions/MarkdownExtensions.cs b/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs
similarity index 50%
rename from src/Extensions/MarkdownExtensions.cs
rename to TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs
index 7b7f780..30ddff5 100644
--- a/src/Extensions/MarkdownExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs
@@ -1,4 +1,4 @@
-namespace Octobot.Extensions;
+namespace TeamOctolings.Octobot.Extensions;
public static class MarkdownExtensions
{
@@ -13,4 +13,16 @@ public static class MarkdownExtensions
{
return $"- {text}";
}
+
+ ///
+ /// Formats a string to use Markdown Quote formatting.
+ ///
+ /// The input text to format.
+ ///
+ /// A markdown-formatted quote string.
+ ///
+ public static string Quote(string text)
+ {
+ return $"> {text}";
+ }
}
diff --git a/TeamOctolings.Octobot/Extensions/ResultExtensions.cs b/TeamOctolings.Octobot/Extensions/ResultExtensions.cs
new file mode 100644
index 0000000..6872d34
--- /dev/null
+++ b/TeamOctolings.Octobot/Extensions/ResultExtensions.cs
@@ -0,0 +1,65 @@
+using System.Diagnostics;
+using Microsoft.Extensions.Logging;
+using Remora.Results;
+
+namespace TeamOctolings.Octobot.Extensions;
+
+public static class ResultExtensions
+{
+ public static Result FromError(Result result)
+ {
+ LogResultStackTrace(result);
+
+ return result;
+ }
+
+ public static Result FromError(Result result)
+ {
+ var casted = (Result)result;
+ LogResultStackTrace(casted);
+
+ return casted;
+ }
+
+ private static void LogResultStackTrace(Result result)
+ {
+ if (result.IsSuccess || result.Error is ExceptionError { Exception: OperationCanceledException })
+ {
+ return;
+ }
+
+ if (Utility.StaticLogger is null)
+ {
+ throw new InvalidOperationException();
+ }
+
+ Utility.StaticLogger.LogError("{ErrorType}: {ErrorMessage}{NewLine}{StackTrace}",
+ result.Error.GetType().FullName, result.Error.Message, Environment.NewLine, ConstructStackTrace());
+
+ var inner = result.Inner;
+ while (inner is { IsSuccess: false })
+ {
+ Utility.StaticLogger.LogError("Caused by: {ResultType}: {ResultMessage}",
+ inner.Error.GetType().FullName, inner.Error.Message);
+
+ inner = inner.Inner;
+ }
+ }
+
+ private static string ConstructStackTrace()
+ {
+ var stackArray = new StackTrace(3, true).ToString().Split(Environment.NewLine).ToList();
+ for (var i = stackArray.Count - 1; i >= 0; i--)
+ {
+ var frame = stackArray[i];
+ var trimmed = frame.TrimStart();
+ if (trimmed.StartsWith("at System.Threading", StringComparison.Ordinal)
+ || trimmed.StartsWith("at System.Runtime.CompilerServices", StringComparison.Ordinal))
+ {
+ stackArray.RemoveAt(i);
+ }
+ }
+
+ return string.Join(Environment.NewLine, stackArray);
+ }
+}
diff --git a/src/Extensions/SnowflakeExtensions.cs b/TeamOctolings.Octobot/Extensions/SnowflakeExtensions.cs
similarity index 96%
rename from src/Extensions/SnowflakeExtensions.cs
rename to TeamOctolings.Octobot/Extensions/SnowflakeExtensions.cs
index e60bc44..70810ef 100644
--- a/src/Extensions/SnowflakeExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/SnowflakeExtensions.cs
@@ -1,6 +1,6 @@
using Remora.Rest.Core;
-namespace Octobot.Extensions;
+namespace TeamOctolings.Octobot.Extensions;
public static class SnowflakeExtensions
{
diff --git a/src/Extensions/StringBuilderExtensions.cs b/TeamOctolings.Octobot/Extensions/StringBuilderExtensions.cs
similarity index 98%
rename from src/Extensions/StringBuilderExtensions.cs
rename to TeamOctolings.Octobot/Extensions/StringBuilderExtensions.cs
index ddd24a3..25b7b5b 100644
--- a/src/Extensions/StringBuilderExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/StringBuilderExtensions.cs
@@ -1,6 +1,6 @@
using System.Text;
-namespace Octobot.Extensions;
+namespace TeamOctolings.Octobot.Extensions;
public static class StringBuilderExtensions
{
diff --git a/src/Extensions/StringExtensions.cs b/TeamOctolings.Octobot/Extensions/StringExtensions.cs
similarity index 98%
rename from src/Extensions/StringExtensions.cs
rename to TeamOctolings.Octobot/Extensions/StringExtensions.cs
index cb8d606..bf7f6c8 100644
--- a/src/Extensions/StringExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/StringExtensions.cs
@@ -1,7 +1,7 @@
using System.Net;
using Remora.Discord.Extensions.Formatting;
-namespace Octobot.Extensions;
+namespace TeamOctolings.Octobot.Extensions;
public static class StringExtensions
{
diff --git a/src/Extensions/UInt64Extensions.cs b/TeamOctolings.Octobot/Extensions/UInt64Extensions.cs
similarity index 82%
rename from src/Extensions/UInt64Extensions.cs
rename to TeamOctolings.Octobot/Extensions/UInt64Extensions.cs
index 5d1db00..2b9c0a2 100644
--- a/src/Extensions/UInt64Extensions.cs
+++ b/TeamOctolings.Octobot/Extensions/UInt64Extensions.cs
@@ -1,7 +1,7 @@
using Remora.Discord.API;
using Remora.Rest.Core;
-namespace Octobot.Extensions;
+namespace TeamOctolings.Octobot.Extensions;
public static class UInt64Extensions
{
diff --git a/src/Extensions/UserExtensions.cs b/TeamOctolings.Octobot/Extensions/UserExtensions.cs
similarity index 85%
rename from src/Extensions/UserExtensions.cs
rename to TeamOctolings.Octobot/Extensions/UserExtensions.cs
index 38fe985..d9eff33 100644
--- a/src/Extensions/UserExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/UserExtensions.cs
@@ -1,6 +1,6 @@
using Remora.Discord.API.Abstractions.Objects;
-namespace Octobot.Extensions;
+namespace TeamOctolings.Octobot.Extensions;
public static class UserExtensions
{
diff --git a/src/Messages.Designer.cs b/TeamOctolings.Octobot/Messages.Designer.cs
similarity index 84%
rename from src/Messages.Designer.cs
rename to TeamOctolings.Octobot/Messages.Designer.cs
index 9597bcd..ce59f1e 100644
--- a/src/Messages.Designer.cs
+++ b/TeamOctolings.Octobot/Messages.Designer.cs
@@ -7,31 +7,34 @@
//
//------------------------------------------------------------------------------
-namespace Octobot {
+namespace TeamOctolings.Octobot {
+ using System;
+
+
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Messages {
-
+
private static System.Resources.ResourceManager resourceMan;
-
+
private static System.Globalization.CultureInfo resourceCulture;
-
+
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Messages() {
}
-
+
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Resources.ResourceManager ResourceManager {
get {
if (object.Equals(null, resourceMan)) {
- System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Octobot.locale.Messages", typeof(Messages).Assembly);
+ System.Resources.ResourceManager temp = new System.Resources.ResourceManager("TeamOctolings.Octobot.Messages", typeof(Messages).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
-
+
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Globalization.CultureInfo Culture {
get {
@@ -41,1142 +44,1163 @@ namespace Octobot {
resourceCulture = value;
}
}
-
+
internal static string Ready {
get {
return ResourceManager.GetString("Ready", resourceCulture);
}
}
-
+
internal static string CachedMessageDeleted {
get {
return ResourceManager.GetString("CachedMessageDeleted", resourceCulture);
}
}
-
+
internal static string CachedMessageEdited {
get {
return ResourceManager.GetString("CachedMessageEdited", resourceCulture);
}
}
-
+
internal static string DefaultWelcomeMessage {
get {
return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture);
}
}
-
- internal static string Sound1 {
+
+ internal static string Generic1 {
get {
- return ResourceManager.GetString("Sound1", resourceCulture);
+ return ResourceManager.GetString("Generic1", resourceCulture);
}
}
-
- internal static string Sound2 {
+
+ internal static string Generic2 {
get {
- return ResourceManager.GetString("Sound2", resourceCulture);
+ return ResourceManager.GetString("Generic2", resourceCulture);
}
}
-
- internal static string Sound3 {
+
+ internal static string Generic3 {
get {
- return ResourceManager.GetString("Sound3", resourceCulture);
+ return ResourceManager.GetString("Generic3", resourceCulture);
}
}
-
+
internal static string YouWereBanned {
get {
return ResourceManager.GetString("YouWereBanned", resourceCulture);
}
}
-
+
internal static string PunishmentExpired {
get {
return ResourceManager.GetString("PunishmentExpired", resourceCulture);
}
}
-
+
internal static string YouWereKicked {
get {
return ResourceManager.GetString("YouWereKicked", resourceCulture);
}
}
-
+
internal static string Milliseconds {
get {
return ResourceManager.GetString("Milliseconds", resourceCulture);
}
}
-
+
internal static string ChannelNotSpecified {
get {
return ResourceManager.GetString("ChannelNotSpecified", resourceCulture);
}
}
-
+
internal static string RoleNotSpecified {
get {
return ResourceManager.GetString("RoleNotSpecified", resourceCulture);
}
}
-
- internal static string SettingsLang {
+
+ internal static string SettingsLanguage {
get {
- return ResourceManager.GetString("SettingsLang", resourceCulture);
+ return ResourceManager.GetString("SettingsLanguage", resourceCulture);
}
}
-
+
internal static string SettingsPrefix {
get {
return ResourceManager.GetString("SettingsPrefix", resourceCulture);
}
}
-
+
internal static string SettingsRemoveRolesOnMute {
get {
return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture);
}
}
-
+
internal static string SettingsSendWelcomeMessages {
get {
return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture);
}
}
-
+
internal static string SettingsMuteRole {
get {
return ResourceManager.GetString("SettingsMuteRole", resourceCulture);
}
}
-
+
internal static string LanguageNotSupported {
get {
return ResourceManager.GetString("LanguageNotSupported", resourceCulture);
}
}
-
+
internal static string Yes {
get {
return ResourceManager.GetString("Yes", resourceCulture);
}
}
-
+
internal static string No {
get {
return ResourceManager.GetString("No", resourceCulture);
}
}
-
+
internal static string UserNotBanned {
get {
return ResourceManager.GetString("UserNotBanned", resourceCulture);
}
}
-
+
internal static string MemberNotMuted {
get {
return ResourceManager.GetString("MemberNotMuted", resourceCulture);
}
}
-
+
internal static string SettingsWelcomeMessage {
get {
return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture);
}
}
-
+
internal static string UserBanned {
get {
return ResourceManager.GetString("UserBanned", resourceCulture);
}
}
-
+
internal static string SettingsReceiveStartupMessages {
get {
return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture);
}
}
-
+
internal static string InvalidSettingValue {
get {
return ResourceManager.GetString("InvalidSettingValue", resourceCulture);
}
}
-
- internal static string InvalidRole {
- get {
- return ResourceManager.GetString("InvalidRole", resourceCulture);
- }
- }
-
- internal static string InvalidChannel {
- get {
- return ResourceManager.GetString("InvalidChannel", resourceCulture);
- }
- }
-
+
internal static string DurationRequiredForTimeOuts {
get {
return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture);
}
}
-
+
internal static string CannotTimeOutBot {
get {
return ResourceManager.GetString("CannotTimeOutBot", resourceCulture);
}
}
-
+
internal static string SettingsEventNotificationRole {
get {
return ResourceManager.GetString("SettingsEventNotificationRole", resourceCulture);
}
}
-
+
internal static string SettingsEventNotificationChannel {
get {
return ResourceManager.GetString("SettingsEventNotificationChannel", resourceCulture);
}
}
-
+
internal static string SettingsEventStartedReceivers {
get {
return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture);
}
}
-
+
internal static string EventStarted {
get {
return ResourceManager.GetString("EventStarted", resourceCulture);
}
}
-
+
internal static string EventCancelled {
get {
return ResourceManager.GetString("EventCancelled", resourceCulture);
}
}
-
+
internal static string EventCompleted {
get {
return ResourceManager.GetString("EventCompleted", resourceCulture);
}
}
-
+
internal static string MessagesCleared {
get {
return ResourceManager.GetString("MessagesCleared", resourceCulture);
}
}
-
+
internal static string SettingsNothingChanged {
get {
return ResourceManager.GetString("SettingsNothingChanged", resourceCulture);
}
}
-
+
internal static string SettingNotDefined {
get {
return ResourceManager.GetString("SettingNotDefined", resourceCulture);
}
}
-
+
+ internal static string MissingUser {
+ get {
+ return ResourceManager.GetString("MissingUser", resourceCulture);
+ }
+ }
+
internal static string UserCannotBanMembers {
get {
return ResourceManager.GetString("UserCannotBanMembers", resourceCulture);
}
}
-
+
internal static string UserCannotManageMessages {
get {
return ResourceManager.GetString("UserCannotManageMessages", resourceCulture);
}
}
-
+
internal static string UserCannotKickMembers {
get {
return ResourceManager.GetString("UserCannotKickMembers", resourceCulture);
}
}
-
- internal static string UserCannotModerateMembers {
+
+ internal static string UserCannotMuteMembers {
get {
- return ResourceManager.GetString("UserCannotModerateMembers", resourceCulture);
+ return ResourceManager.GetString("UserCannotMuteMembers", resourceCulture);
}
}
-
+
+ internal static string UserCannotUnmuteMembers {
+ get {
+ return ResourceManager.GetString("UserCannotUnmuteMembers", resourceCulture);
+ }
+ }
+
internal static string UserCannotManageGuild {
get {
return ResourceManager.GetString("UserCannotManageGuild", resourceCulture);
}
}
-
+
internal static string BotCannotBanMembers {
get {
return ResourceManager.GetString("BotCannotBanMembers", resourceCulture);
}
}
-
+
internal static string BotCannotManageMessages {
get {
return ResourceManager.GetString("BotCannotManageMessages", resourceCulture);
}
}
-
+
internal static string BotCannotKickMembers {
get {
return ResourceManager.GetString("BotCannotKickMembers", resourceCulture);
}
}
-
+
internal static string BotCannotModerateMembers {
get {
return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture);
}
}
-
+
internal static string BotCannotManageGuild {
get {
return ResourceManager.GetString("BotCannotManageGuild", resourceCulture);
}
}
-
+
internal static string UserCannotBanOwner {
get {
return ResourceManager.GetString("UserCannotBanOwner", resourceCulture);
}
}
-
+
internal static string UserCannotBanThemselves {
get {
return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture);
}
}
-
+
internal static string UserCannotBanBot {
get {
return ResourceManager.GetString("UserCannotBanBot", resourceCulture);
}
}
-
+
internal static string BotCannotBanTarget {
get {
return ResourceManager.GetString("BotCannotBanTarget", resourceCulture);
}
}
-
+
internal static string UserCannotBanTarget {
get {
return ResourceManager.GetString("UserCannotBanTarget", resourceCulture);
}
}
-
+
internal static string UserCannotKickOwner {
get {
return ResourceManager.GetString("UserCannotKickOwner", resourceCulture);
}
}
-
+
internal static string UserCannotKickThemselves {
get {
return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture);
}
}
-
+
internal static string UserCannotKickBot {
get {
return ResourceManager.GetString("UserCannotKickBot", resourceCulture);
}
}
-
+
internal static string BotCannotKickTarget {
get {
return ResourceManager.GetString("BotCannotKickTarget", resourceCulture);
}
}
-
+
internal static string UserCannotKickTarget {
get {
return ResourceManager.GetString("UserCannotKickTarget", resourceCulture);
}
}
-
+
internal static string UserCannotMuteOwner {
get {
return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture);
}
}
-
+
internal static string UserCannotMuteThemselves {
get {
return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture);
}
}
-
+
internal static string UserCannotMuteBot {
get {
return ResourceManager.GetString("UserCannotMuteBot", resourceCulture);
}
}
-
+
internal static string BotCannotMuteTarget {
get {
return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture);
}
}
-
+
internal static string UserCannotMuteTarget {
get {
return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture);
}
}
-
+
internal static string UserCannotUnmuteOwner {
get {
return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture);
}
}
-
+
internal static string UserCannotUnmuteThemselves {
get {
return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture);
}
}
-
+
internal static string UserCannotUnmuteBot {
get {
return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture);
}
}
-
+
internal static string BotCannotUnmuteTarget {
get {
return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture);
}
}
-
+
internal static string UserCannotUnmuteTarget {
get {
return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture);
}
}
-
+
internal static string EventEarlyNotification {
get {
return ResourceManager.GetString("EventEarlyNotification", resourceCulture);
}
}
-
+
internal static string SettingsEventEarlyNotificationOffset {
get {
return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture);
}
}
-
+
internal static string UserNotFound {
get {
return ResourceManager.GetString("UserNotFound", resourceCulture);
}
}
-
+
internal static string SettingsDefaultRole {
get {
return ResourceManager.GetString("SettingsDefaultRole", resourceCulture);
}
}
-
+
internal static string SettingsPublicFeedbackChannel {
get {
return ResourceManager.GetString("SettingsPublicFeedbackChannel", resourceCulture);
}
}
-
+
internal static string SettingsPrivateFeedbackChannel {
get {
return ResourceManager.GetString("SettingsPrivateFeedbackChannel", resourceCulture);
}
}
-
+
internal static string SettingsReturnRolesOnRejoin {
get {
return ResourceManager.GetString("SettingsReturnRolesOnRejoin", resourceCulture);
}
}
-
+
internal static string SettingsAutoStartEvents {
get {
return ResourceManager.GetString("SettingsAutoStartEvents", resourceCulture);
}
}
-
+
internal static string IssuedBy {
get {
return ResourceManager.GetString("IssuedBy", resourceCulture);
}
}
-
+
internal static string EventCreatedTitle {
get {
return ResourceManager.GetString("EventCreatedTitle", resourceCulture);
}
}
-
+
internal static string DescriptionLocalEventCreated {
get {
return ResourceManager.GetString("DescriptionLocalEventCreated", resourceCulture);
}
}
-
+
internal static string DescriptionExternalEventCreated {
get {
return ResourceManager.GetString("DescriptionExternalEventCreated", resourceCulture);
}
}
-
+
internal static string ButtonOpenEventInfo {
get {
return ResourceManager.GetString("ButtonOpenEventInfo", resourceCulture);
}
}
-
+
internal static string EventDuration {
get {
return ResourceManager.GetString("EventDuration", resourceCulture);
}
}
-
+
internal static string DescriptionLocalEventStarted {
get {
return ResourceManager.GetString("DescriptionLocalEventStarted", resourceCulture);
}
}
-
+
internal static string DescriptionExternalEventStarted {
get {
return ResourceManager.GetString("DescriptionExternalEventStarted", resourceCulture);
}
}
-
+
internal static string UserAlreadyBanned {
get {
return ResourceManager.GetString("UserAlreadyBanned", resourceCulture);
}
}
-
+
internal static string UserUnbanned {
get {
return ResourceManager.GetString("UserUnbanned", resourceCulture);
}
}
-
+
internal static string UserMuted {
get {
return ResourceManager.GetString("UserMuted", resourceCulture);
}
}
-
+
internal static string UserUnmuted {
get {
return ResourceManager.GetString("UserUnmuted", resourceCulture);
}
}
-
+
internal static string UserNotMuted {
get {
return ResourceManager.GetString("UserNotMuted", resourceCulture);
}
}
-
+
internal static string UserNotFoundShort {
get {
return ResourceManager.GetString("UserNotFoundShort", resourceCulture);
}
}
-
+
internal static string UserKicked {
get {
return ResourceManager.GetString("UserKicked", resourceCulture);
}
}
-
+
internal static string DescriptionActionReason {
get {
return ResourceManager.GetString("DescriptionActionReason", resourceCulture);
}
}
-
+
internal static string DescriptionActionExpiresAt {
get {
return ResourceManager.GetString("DescriptionActionExpiresAt", resourceCulture);
}
}
-
+
internal static string UserAlreadyMuted {
get {
return ResourceManager.GetString("UserAlreadyMuted", resourceCulture);
}
}
-
+
internal static string MessageFrom {
get {
return ResourceManager.GetString("MessageFrom", resourceCulture);
}
}
-
+
internal static string AboutTitleDevelopers {
get {
return ResourceManager.GetString("AboutTitleDevelopers", resourceCulture);
}
}
-
+
internal static string ButtonOpenRepository {
get {
return ResourceManager.GetString("ButtonOpenRepository", resourceCulture);
}
}
-
+
internal static string AboutBot {
get {
return ResourceManager.GetString("AboutBot", resourceCulture);
}
}
-
+
internal static string AboutDeveloper_mctaylors {
get {
return ResourceManager.GetString("AboutDeveloper@mctaylors", resourceCulture);
}
}
-
+
internal static string AboutDeveloper_Octol1ttle {
get {
return ResourceManager.GetString("AboutDeveloper@Octol1ttle", resourceCulture);
}
}
-
+
internal static string AboutDeveloper_neroduckale {
get {
return ResourceManager.GetString("AboutDeveloper@neroduckale", resourceCulture);
}
}
-
+
internal static string ReminderCreated {
get {
return ResourceManager.GetString("ReminderCreated", resourceCulture);
}
}
-
+
internal static string Reminder {
get {
return ResourceManager.GetString("Reminder", resourceCulture);
}
}
-
+
internal static string DescriptionReminder {
get {
return ResourceManager.GetString("DescriptionReminder", resourceCulture);
}
}
-
+
internal static string SettingsListTitle {
get {
return ResourceManager.GetString("SettingsListTitle", resourceCulture);
}
}
-
+
internal static string SettingSuccessfullyChanged {
get {
return ResourceManager.GetString("SettingSuccessfullyChanged", resourceCulture);
}
}
-
+
internal static string SettingNotChanged {
get {
return ResourceManager.GetString("SettingNotChanged", resourceCulture);
}
}
-
+
internal static string SettingIsNow {
get {
return ResourceManager.GetString("SettingIsNow", resourceCulture);
}
}
-
+
+ internal static string SettingsRenameHoistedUsers {
+ get {
+ return ResourceManager.GetString("SettingsRenameHoistedUsers", resourceCulture);
+ }
+ }
+
internal static string Page {
get {
return ResourceManager.GetString("Page", resourceCulture);
}
}
-
+
internal static string PageNotFound {
get {
return ResourceManager.GetString("PageNotFound", resourceCulture);
}
}
-
+
internal static string PagesAllowed {
get {
return ResourceManager.GetString("PagesAllowed", resourceCulture);
}
}
-
+
internal static string Next {
get {
return ResourceManager.GetString("Next", resourceCulture);
}
}
-
+
internal static string Previous {
get {
return ResourceManager.GetString("Previous", resourceCulture);
}
}
-
+
internal static string ReminderList {
get {
return ResourceManager.GetString("ReminderList", resourceCulture);
}
}
-
+
internal static string InvalidReminderPosition {
get {
return ResourceManager.GetString("InvalidReminderPosition", resourceCulture);
}
}
-
+
internal static string ReminderDeleted {
get {
return ResourceManager.GetString("ReminderDeleted", resourceCulture);
}
}
-
+
internal static string NoRemindersFound {
get {
return ResourceManager.GetString("NoRemindersFound", resourceCulture);
}
}
-
+
internal static string SingleSettingReset {
get {
return ResourceManager.GetString("SingleSettingReset", resourceCulture);
}
}
-
+
internal static string AllSettingsReset {
get {
return ResourceManager.GetString("AllSettingsReset", resourceCulture);
}
}
-
+
internal static string DescriptionActionJumpToMessage {
get {
return ResourceManager.GetString("DescriptionActionJumpToMessage", resourceCulture);
}
}
-
+
internal static string DescriptionActionJumpToChannel {
get {
return ResourceManager.GetString("DescriptionActionJumpToChannel", resourceCulture);
}
}
-
+
internal static string ReminderPosition {
get {
return ResourceManager.GetString("ReminderPosition", resourceCulture);
}
}
-
+
internal static string ReminderTime {
get {
return ResourceManager.GetString("ReminderTime", resourceCulture);
}
}
-
+
internal static string ReminderText {
get {
return ResourceManager.GetString("ReminderText", resourceCulture);
}
}
-
- internal static string InformationAbout {
- get {
- return ResourceManager.GetString("InformationAbout", resourceCulture);
- }
- }
-
+
internal static string UserInfoDisplayName {
get {
return ResourceManager.GetString("UserInfoDisplayName", resourceCulture);
}
}
-
- internal static string UserInfoDiscordUserSince {
+
+ internal static string InformationAbout {
get {
- return ResourceManager.GetString("UserInfoDiscordUserSince", resourceCulture);
+ return ResourceManager.GetString("InformationAbout", resourceCulture);
}
}
-
+
internal static string UserInfoMuted {
get {
return ResourceManager.GetString("UserInfoMuted", resourceCulture);
}
}
-
+
+ internal static string UserInfoDiscordUserSince {
+ get {
+ return ResourceManager.GetString("UserInfoDiscordUserSince", resourceCulture);
+ }
+ }
+
internal static string UserInfoBanned {
get {
return ResourceManager.GetString("UserInfoBanned", resourceCulture);
}
}
-
+
internal static string UserInfoPunishments {
get {
return ResourceManager.GetString("UserInfoPunishments", resourceCulture);
}
}
-
+
internal static string UserInfoBannedPermanently {
get {
return ResourceManager.GetString("UserInfoBannedPermanently", resourceCulture);
}
}
-
+
internal static string UserInfoNotOnGuild {
get {
return ResourceManager.GetString("UserInfoNotOnGuild", resourceCulture);
}
}
-
+
internal static string UserInfoMutedByTimeout {
get {
return ResourceManager.GetString("UserInfoMutedByTimeout", resourceCulture);
}
}
-
+
internal static string UserInfoMutedByMuteRole {
get {
return ResourceManager.GetString("UserInfoMutedByMuteRole", resourceCulture);
}
}
-
+
internal static string UserInfoGuildMemberSince {
get {
return ResourceManager.GetString("UserInfoGuildMemberSince", resourceCulture);
}
}
-
+
internal static string UserInfoGuildNickname {
get {
return ResourceManager.GetString("UserInfoGuildNickname", resourceCulture);
}
}
-
+
internal static string UserInfoGuildRoles {
get {
return ResourceManager.GetString("UserInfoGuildRoles", resourceCulture);
}
}
-
+
internal static string UserInfoGuildMemberPremiumSince {
get {
return ResourceManager.GetString("UserInfoGuildMemberPremiumSince", resourceCulture);
}
}
-
- internal static string RandomTitle
- {
+
+ internal static string RandomTitle {
get {
return ResourceManager.GetString("RandomTitle", resourceCulture);
}
}
-
- internal static string RandomMinMaxSame
- {
+
+ internal static string RandomMinMaxSame {
get {
return ResourceManager.GetString("RandomMinMaxSame", resourceCulture);
}
}
-
- internal static string RandomMax
- {
- get {
- return ResourceManager.GetString("RandomMax", resourceCulture);
- }
- }
-
- internal static string RandomMin
- {
+
+ internal static string RandomMin {
get {
return ResourceManager.GetString("RandomMin", resourceCulture);
}
}
-
- internal static string Default
- {
+
+ internal static string RandomMax {
+ get {
+ return ResourceManager.GetString("RandomMax", resourceCulture);
+ }
+ }
+
+ internal static string Default {
get {
return ResourceManager.GetString("Default", resourceCulture);
}
}
-
- internal static string TimestampTitle
- {
- get
- {
+
+ internal static string TimestampTitle {
+ get {
return ResourceManager.GetString("TimestampTitle", resourceCulture);
}
}
-
- internal static string TimestampOffset
- {
- get
- {
+
+ internal static string TimestampOffset {
+ get {
return ResourceManager.GetString("TimestampOffset", resourceCulture);
}
}
-
- internal static string GuildInfoDescription
- {
- get
- {
+
+ internal static string GuildInfoDescription {
+ get {
return ResourceManager.GetString("GuildInfoDescription", resourceCulture);
}
}
-
- internal static string GuildInfoCreatedAt
- {
- get
- {
+
+ internal static string GuildInfoCreatedAt {
+ get {
return ResourceManager.GetString("GuildInfoCreatedAt", resourceCulture);
}
}
-
- internal static string GuildInfoOwner
- {
- get
- {
+
+ internal static string GuildInfoOwner {
+ get {
return ResourceManager.GetString("GuildInfoOwner", resourceCulture);
}
}
-
- internal static string GuildInfoServerBoost
- {
- get
- {
+
+ internal static string GuildInfoServerBoost {
+ get {
return ResourceManager.GetString("GuildInfoServerBoost", resourceCulture);
}
}
-
- internal static string GuildInfoBoostTier
- {
- get
- {
+
+ internal static string GuildInfoBoostTier {
+ get {
return ResourceManager.GetString("GuildInfoBoostTier", resourceCulture);
}
}
-
- internal static string GuildInfoBoostCount
- {
- get
- {
+
+ internal static string GuildInfoBoostCount {
+ get {
return ResourceManager.GetString("GuildInfoBoostCount", resourceCulture);
}
}
-
- internal static string NoMessagesToClear
- {
- get
- {
+
+ internal static string NoMessagesToClear {
+ get {
return ResourceManager.GetString("NoMessagesToClear", resourceCulture);
}
}
-
- internal static string MessagesClearedFiltered
- {
- get
- {
+
+ internal static string MessagesClearedFiltered {
+ get {
return ResourceManager.GetString("MessagesClearedFiltered", resourceCulture);
}
}
-
- internal static string DataLoadFailedTitle
- {
- get
- {
+
+ internal static string DataLoadFailedTitle {
+ get {
return ResourceManager.GetString("DataLoadFailedTitle", resourceCulture);
}
}
-
- internal static string DataLoadFailedDescription
- {
- get
- {
+
+ internal static string DataLoadFailedDescription {
+ get {
return ResourceManager.GetString("DataLoadFailedDescription", resourceCulture);
}
}
-
- internal static string CommandExecutionFailed
- {
- get
- {
+
+ internal static string CommandExecutionFailed {
+ get {
return ResourceManager.GetString("CommandExecutionFailed", resourceCulture);
}
}
-
- internal static string ContactDevelopers
- {
- get
- {
+
+ internal static string ContactDevelopers {
+ get {
return ResourceManager.GetString("ContactDevelopers", resourceCulture);
}
}
-
- internal static string ButtonReportIssue
- {
- get
- {
+
+ internal static string ButtonReportIssue {
+ get {
return ResourceManager.GetString("ButtonReportIssue", resourceCulture);
}
}
-
- internal static string InvalidTimeSpan
- {
- get
- {
+
+ internal static string DefaultLeaveMessage {
+ get {
+ return ResourceManager.GetString("DefaultLeaveMessage", resourceCulture);
+ }
+ }
+
+ internal static string SettingsLeaveMessage {
+ get {
+ return ResourceManager.GetString("SettingsLeaveMessage", resourceCulture);
+ }
+ }
+
+ internal static string InvalidTimeSpan {
+ get {
return ResourceManager.GetString("InvalidTimeSpan", resourceCulture);
}
}
-
- internal static string UserInfoKicked
- {
- get
- {
+
+ internal static string UserInfoKicked {
+ get {
return ResourceManager.GetString("UserInfoKicked", resourceCulture);
}
}
-
+
internal static string ReminderEdited {
get {
return ResourceManager.GetString("ReminderEdited", resourceCulture);
}
}
-
+
internal static string EightBallPositive1 {
get {
return ResourceManager.GetString("EightBallPositive1", resourceCulture);
}
}
-
+
internal static string EightBallPositive2 {
get {
return ResourceManager.GetString("EightBallPositive2", resourceCulture);
}
}
-
+
internal static string EightBallPositive3 {
get {
return ResourceManager.GetString("EightBallPositive3", resourceCulture);
}
}
-
+
internal static string EightBallPositive4 {
get {
return ResourceManager.GetString("EightBallPositive4", resourceCulture);
}
}
-
+
internal static string EightBallPositive5 {
get {
return ResourceManager.GetString("EightBallPositive5", resourceCulture);
}
}
-
+
internal static string EightBallQuestionable1 {
get {
return ResourceManager.GetString("EightBallQuestionable1", resourceCulture);
}
}
-
+
internal static string EightBallQuestionable2 {
get {
return ResourceManager.GetString("EightBallQuestionable2", resourceCulture);
}
}
-
+
internal static string EightBallQuestionable3 {
get {
return ResourceManager.GetString("EightBallQuestionable3", resourceCulture);
}
}
-
+
internal static string EightBallQuestionable4 {
get {
return ResourceManager.GetString("EightBallQuestionable4", resourceCulture);
}
}
-
+
internal static string EightBallQuestionable5 {
get {
return ResourceManager.GetString("EightBallQuestionable5", resourceCulture);
}
}
-
+
internal static string EightBallNeutral1 {
get {
return ResourceManager.GetString("EightBallNeutral1", resourceCulture);
}
}
-
+
internal static string EightBallNeutral2 {
get {
return ResourceManager.GetString("EightBallNeutral2", resourceCulture);
}
}
-
+
internal static string EightBallNeutral3 {
get {
return ResourceManager.GetString("EightBallNeutral3", resourceCulture);
}
}
-
+
internal static string EightBallNeutral4 {
get {
return ResourceManager.GetString("EightBallNeutral4", resourceCulture);
}
}
-
+
internal static string EightBallNeutral5 {
get {
return ResourceManager.GetString("EightBallNeutral5", resourceCulture);
}
}
-
+
internal static string EightBallNegative1 {
get {
return ResourceManager.GetString("EightBallNegative1", resourceCulture);
}
}
-
+
internal static string EightBallNegative2 {
get {
return ResourceManager.GetString("EightBallNegative2", resourceCulture);
}
}
-
+
internal static string EightBallNegative3 {
get {
return ResourceManager.GetString("EightBallNegative3", resourceCulture);
}
}
-
+
internal static string EightBallNegative4 {
get {
return ResourceManager.GetString("EightBallNegative4", resourceCulture);
}
}
-
+
internal static string EightBallNegative5 {
get {
return ResourceManager.GetString("EightBallNegative5", resourceCulture);
}
}
+
+ internal static string TimeSpanExample {
+ get {
+ return ResourceManager.GetString("TimeSpanExample", resourceCulture);
+ }
+ }
+
+ internal static string Version {
+ get {
+ return ResourceManager.GetString("Version", resourceCulture);
+ }
+ }
+
+ internal static string SettingsWelcomeMessagesChannel {
+ get {
+ return ResourceManager.GetString("SettingsWelcomeMessagesChannel", resourceCulture);
+ }
+ }
+
+ internal static string ButtonDirty {
+ get {
+ return ResourceManager.GetString("ButtonDirty", resourceCulture);
+ }
+ }
+
+ internal static string ButtonOpenWiki {
+ get {
+ return ResourceManager.GetString("ButtonOpenWiki", resourceCulture);
+ }
+ }
+
+ internal static string SettingsModeratorRole {
+ get {
+ return ResourceManager.GetString("SettingsModeratorRole", resourceCulture);
+ }
+ }
+
+ internal static string SettingValueEquals {
+ get {
+ return ResourceManager.GetString("SettingValueEquals", resourceCulture);
+ }
+ }
}
}
diff --git a/locale/Messages.resx b/TeamOctolings.Octobot/Messages.resx
similarity index 94%
rename from locale/Messages.resx
rename to TeamOctolings.Octobot/Messages.resx
index b881996..059584a 100644
--- a/locale/Messages.resx
+++ b/TeamOctolings.Octobot/Messages.resx
@@ -117,13 +117,13 @@
{0}, welcome to {1}
-
+
Veemo!
-
+
Woomy!
-
+
Ngyes!
@@ -231,8 +231,11 @@
You cannot kick members from this guild!
-
- You cannot moderate members in this guild!
+
+ You cannot mute members in this guild!
+
+
+ You cannot unmute members in this guild!
You cannot manage this guild!
@@ -585,6 +588,12 @@
Report an issue
+
+ See you soon, {0}!
+
+
+ Leave message
+
Time specified incorrectly!
@@ -654,4 +663,25 @@
Very doubtful
+
+ Example of a valid input: `1h30m`
+
+
+ Version: {0}
+
+
+ Welcome messages channel
+
+
+ Can't report an issue in the development version
+
+
+ Open Octobot's Wiki
+
+
+ Moderator role
+
+
+ The setting value is the same as the input value.
+
diff --git a/locale/Messages.ru.resx b/TeamOctolings.Octobot/Messages.ru.resx
similarity index 94%
rename from locale/Messages.ru.resx
rename to TeamOctolings.Octobot/Messages.ru.resx
index cb318cd..fc8a594 100644
--- a/locale/Messages.ru.resx
+++ b/TeamOctolings.Octobot/Messages.ru.resx
@@ -117,13 +117,13 @@
{0}, добро пожаловать на сервер {1}
-
+
Виимо!
-
+
Вууми!
-
+
Нгьес!
@@ -228,8 +228,11 @@
Ты не можешь выгонять участников с этого сервера!
-
- Ты не можешь модерировать участников этого сервера!
+
+ Ты не можешь глушить участников этого сервера!
+
+
+ Ты не можешь разглушать участников этого сервера!
Ты не можешь настраивать этот сервер!
@@ -585,6 +588,12 @@
Сообщить о проблеме
+
+ До скорой встречи, {0}!
+
+
+ Сообщение о выходе
+
Неправильно указано время!
@@ -654,4 +663,25 @@
Весьма сомнительно
+
+ Пример правильного ввода: `1ч30м`
+
+
+ Версия: {0}
+
+
+ Канал для приветствий
+
+
+ Нельзя сообщить о проблеме в версии под разработкой
+
+
+ Открыть Octobot's Wiki
+
+
+ Роль модератора
+
+
+ Значение настройки такое же, как и вводное значение.
+
diff --git a/src/Parsers/TimeSpanParser.cs b/TeamOctolings.Octobot/Parsers/TimeSpanParser.cs
similarity index 98%
rename from src/Parsers/TimeSpanParser.cs
rename to TeamOctolings.Octobot/Parsers/TimeSpanParser.cs
index 1f44d46..99a8b90 100644
--- a/src/Parsers/TimeSpanParser.cs
+++ b/TeamOctolings.Octobot/Parsers/TimeSpanParser.cs
@@ -4,7 +4,7 @@ using JetBrains.Annotations;
using Remora.Commands.Parsers;
using Remora.Results;
-namespace Octobot.Parsers;
+namespace TeamOctolings.Octobot.Parsers;
///
/// Parses s.
diff --git a/src/Octobot.cs b/TeamOctolings.Octobot/Program.cs
similarity index 87%
rename from src/Octobot.cs
rename to TeamOctolings.Octobot/Program.cs
index 1ebf7c3..d1d6220 100644
--- a/src/Octobot.cs
+++ b/TeamOctolings.Octobot/Program.cs
@@ -2,12 +2,8 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-using Octobot.Commands.Events;
-using Octobot.Services;
-using Octobot.Services.Update;
using Remora.Discord.API.Abstractions.Gateway.Commands;
using Remora.Discord.API.Abstractions.Objects;
-using Remora.Discord.API.Objects;
using Remora.Discord.Caching.Extensions;
using Remora.Discord.Caching.Services;
using Remora.Discord.Commands.Extensions;
@@ -15,23 +11,20 @@ using Remora.Discord.Commands.Services;
using Remora.Discord.Extensions.Extensions;
using Remora.Discord.Gateway;
using Remora.Discord.Hosting.Extensions;
-using Remora.Rest.Core;
using Serilog.Extensions.Logging;
+using TeamOctolings.Octobot.Commands.Events;
+using TeamOctolings.Octobot.Services;
+using TeamOctolings.Octobot.Services.Update;
-namespace Octobot;
+namespace TeamOctolings.Octobot;
-public sealed class Octobot
+public sealed class Program
{
- public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot";
- public const string IssuesUrl = $"{RepositoryUrl}/issues";
-
- public static readonly AllowedMentions NoMentions = new(
- Array.Empty(), Array.Empty(), Array.Empty());
-
public static async Task Main(string[] args)
{
var host = CreateHostBuilder(args).UseConsoleLifetime().Build();
var services = host.Services;
+ Utility.StaticLogger = services.GetRequiredService>();
var slashService = services.GetRequiredService();
// Providing a guild ID to this call will result in command duplicates!
@@ -80,14 +73,15 @@ public sealed class Octobot
// Init
.AddDiscordCaching()
.AddDiscordCommands(true, false)
- .AddRespondersFromAssembly(typeof(Octobot).Assembly)
- .AddCommandGroupsFromAssembly(typeof(Octobot).Assembly)
+ .AddRespondersFromAssembly(typeof(Program).Assembly)
+ .AddCommandGroupsFromAssembly(typeof(Program).Assembly)
// Slash command event handlers
.AddPreparationErrorEvent()
.AddPostExecutionEvent()
// Services
- .AddSingleton()
+ .AddSingleton()
.AddSingleton()
+ .AddSingleton()
.AddHostedService(provider => provider.GetRequiredService())
.AddHostedService()
.AddHostedService()
diff --git a/src/Responders/GuildLoadedResponder.cs b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs
similarity index 80%
rename from src/Responders/GuildLoadedResponder.cs
rename to TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs
index a1e7d16..b420db2 100644
--- a/src/Responders/GuildLoadedResponder.cs
+++ b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs
@@ -1,8 +1,5 @@
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
-using Octobot.Data;
-using Octobot.Extensions;
-using Octobot.Services;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
@@ -11,15 +8,18 @@ using Remora.Discord.API.Objects;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Gateway.Responders;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Services;
-namespace Octobot.Responders;
+namespace TeamOctolings.Octobot.Responders;
///
/// Handles sending a message to a guild that has just initialized if that guild
/// has enabled
///
[UsedImplicitly]
-public class GuildLoadedResponder : IResponder
+public sealed class GuildLoadedResponder : IResponder
{
private readonly IDiscordRestChannelAPI _channelApi;
private readonly GuildDataService _guildData;
@@ -42,7 +42,7 @@ public class GuildLoadedResponder : IResponder
{
if (!gatewayEvent.Guild.IsT0) // Guild is not IAvailableGuild
{
- return Result.FromSuccess();
+ return Result.Success;
}
var guild = gatewayEvent.Guild.AsT0;
@@ -57,7 +57,7 @@ public class GuildLoadedResponder : IResponder
var botResult = await _userApi.GetCurrentUserAsync(ct);
if (!botResult.IsDefined(out var bot))
{
- return Result.FromError(botResult);
+ return ResultExtensions.FromError(botResult);
}
if (data.DataLoadFailed)
@@ -68,27 +68,23 @@ public class GuildLoadedResponder : IResponder
var ownerResult = await _userApi.GetUserAsync(guild.OwnerID, ct);
if (!ownerResult.IsDefined(out var owner))
{
- return Result.FromError(ownerResult);
+ return ResultExtensions.FromError(ownerResult);
}
_logger.LogInformation("Loaded guild \"{Name}\" ({ID}) owned by {Owner} ({OwnerID}) with {MemberCount} members",
guild.Name, guild.ID, owner.GetTag(), owner.ID, guild.MemberCount);
- if (!GuildSettings.ReceiveStartupMessages.Get(cfg))
+ if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()
+ || !GuildSettings.ReceiveStartupMessages.Get(cfg))
{
- return Result.FromSuccess();
- }
-
- if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
- {
- return Result.FromSuccess();
+ return Result.Success;
}
Messages.Culture = GuildSettings.Language.Get(cfg);
var i = Random.Shared.Next(1, 4);
var embed = new EmbedBuilder().WithSmallTitle(bot.GetTag(), bot)
- .WithTitle($"Sound{i}".Localized())
+ .WithTitle($"Generic{i}".Localized())
.WithDescription(Messages.Ready)
.WithCurrentTimestamp()
.WithColour(ColorsList.Blue)
@@ -98,12 +94,12 @@ public class GuildLoadedResponder : IResponder
GuildSettings.PrivateFeedbackChannel.Get(cfg), embedResult: embed, ct: ct);
}
- private async Task SendDataLoadFailed(IGuild guild, GuildData data, IUser bot, CancellationToken ct)
+ private async Task SendDataLoadFailed(IGuild guild, GuildData data, IUser bot, CancellationToken ct = default)
{
var channelResult = await _utility.GetEmergencyFeedbackChannel(guild, data, ct);
if (!channelResult.IsDefined(out var channel))
{
- return Result.FromError(channelResult);
+ return ResultExtensions.FromError(channelResult);
}
var errorEmbed = new EmbedBuilder()
@@ -115,9 +111,12 @@ public class GuildLoadedResponder : IResponder
var issuesButton = new ButtonComponent(
ButtonComponentStyle.Link,
- Messages.ButtonReportIssue,
- new PartialEmoji(Name: "⚠️"),
- URL: Octobot.IssuesUrl
+ BuildInfo.IsDirty
+ ? Messages.ButtonDirty
+ : Messages.ButtonReportIssue,
+ new PartialEmoji(Name: "\u26a0\ufe0f"), // 'WARNING SIGN' (U+26A0)
+ URL: BuildInfo.IssuesUrl,
+ IsDisabled: BuildInfo.IsDirty
);
return await _channelApi.CreateMessageWithEmbedResultAsync(channel, embedResult: errorEmbed,
diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs
similarity index 82%
rename from src/Responders/GuildMemberJoinedResponder.cs
rename to TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs
index eee93b6..ae9f174 100644
--- a/src/Responders/GuildMemberJoinedResponder.cs
+++ b/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs
@@ -1,16 +1,16 @@
using System.Text.Json.Nodes;
using JetBrains.Annotations;
-using Octobot.Data;
-using Octobot.Extensions;
-using Octobot.Services;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Gateway.Responders;
using Remora.Rest.Core;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Services;
-namespace Octobot.Responders;
+namespace TeamOctolings.Octobot.Responders;
///
/// Handles sending a guild's if one is set.
@@ -18,7 +18,7 @@ namespace Octobot.Responders;
///
///
[UsedImplicitly]
-public class GuildMemberJoinedResponder : IResponder
+public sealed class GuildMemberJoinedResponder : IResponder
{
private readonly IDiscordRestChannelAPI _channelApi;
private readonly IDiscordRestGuildAPI _guildApi;
@@ -48,13 +48,13 @@ public class GuildMemberJoinedResponder : IResponder
var returnRolesResult = await TryReturnRolesAsync(cfg, memberData, gatewayEvent.GuildID, user.ID, ct);
if (!returnRolesResult.IsSuccess)
{
- return Result.FromError(returnRolesResult.Error);
+ return ResultExtensions.FromError(returnRolesResult);
}
- if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
+ if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty()
|| GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled")
{
- return Result.FromSuccess();
+ return Result.Success;
}
Messages.Culture = GuildSettings.Language.Get(cfg);
@@ -65,7 +65,7 @@ public class GuildMemberJoinedResponder : IResponder
var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct);
if (!guildResult.IsDefined(out var guild))
{
- return Result.FromError(guildResult);
+ return ResultExtensions.FromError(guildResult);
}
var embed = new EmbedBuilder()
@@ -76,16 +76,16 @@ public class GuildMemberJoinedResponder : IResponder
.Build();
return await _channelApi.CreateMessageWithEmbedResultAsync(
- GuildSettings.PublicFeedbackChannel.Get(cfg), embedResult: embed,
- allowedMentions: Octobot.NoMentions, ct: ct);
+ GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed,
+ allowedMentions: Utility.NoMentions, ct: ct);
}
private async Task TryReturnRolesAsync(
- JsonNode cfg, MemberData memberData, Snowflake guildId, Snowflake userId, CancellationToken ct)
+ JsonNode cfg, MemberData memberData, Snowflake guildId, Snowflake userId, CancellationToken ct = default)
{
if (!GuildSettings.ReturnRolesOnRejoin.Get(cfg))
{
- return Result.FromSuccess();
+ return Result.Success;
}
var assignRoles = new List();
diff --git a/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs b/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs
new file mode 100644
index 0000000..9774899
--- /dev/null
+++ b/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs
@@ -0,0 +1,72 @@
+using JetBrains.Annotations;
+using Remora.Discord.API.Abstractions.Gateway.Events;
+using Remora.Discord.API.Abstractions.Rest;
+using Remora.Discord.Extensions.Embeds;
+using Remora.Discord.Gateway.Responders;
+using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Services;
+
+namespace TeamOctolings.Octobot.Responders;
+
+///
+/// Handles sending a guild's if one is set.
+///
+///
+[UsedImplicitly]
+public sealed class GuildMemberLeftResponder : IResponder
+{
+ private readonly IDiscordRestChannelAPI _channelApi;
+ private readonly IDiscordRestGuildAPI _guildApi;
+ private readonly GuildDataService _guildData;
+
+ public GuildMemberLeftResponder(
+ IDiscordRestChannelAPI channelApi, GuildDataService guildData, IDiscordRestGuildAPI guildApi)
+ {
+ _channelApi = channelApi;
+ _guildData = guildData;
+ _guildApi = guildApi;
+ }
+
+ public async Task RespondAsync(IGuildMemberRemove gatewayEvent, CancellationToken ct = default)
+ {
+ var user = gatewayEvent.User;
+ var data = await _guildData.GetData(gatewayEvent.GuildID, ct);
+ var cfg = data.Settings;
+
+ var memberData = data.GetOrCreateMemberData(user.ID);
+ if (memberData.BannedUntil is not null || memberData.Kicked)
+ {
+ return Result.Success;
+ }
+
+ if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty()
+ || GuildSettings.LeaveMessage.Get(cfg) is "off" or "disable" or "disabled")
+ {
+ return Result.Success;
+ }
+
+ Messages.Culture = GuildSettings.Language.Get(cfg);
+ var leaveMessage = GuildSettings.LeaveMessage.Get(cfg) is "default" or "reset"
+ ? Messages.DefaultLeaveMessage
+ : GuildSettings.LeaveMessage.Get(cfg);
+
+ var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct);
+ if (!guildResult.IsDefined(out var guild))
+ {
+ return ResultExtensions.FromError(guildResult);
+ }
+
+ var embed = new EmbedBuilder()
+ .WithSmallTitle(string.Format(leaveMessage, user.GetTag(), guild.Name), user)
+ .WithGuildFooter(guild)
+ .WithTimestamp(DateTimeOffset.UtcNow)
+ .WithColour(ColorsList.Black)
+ .Build();
+
+ return await _channelApi.CreateMessageWithEmbedResultAsync(
+ GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed,
+ allowedMentions: Utility.NoMentions, ct: ct);
+ }
+}
diff --git a/src/Responders/GuildUnloadedResponder.cs b/TeamOctolings.Octobot/Responders/GuildUnloadedResponder.cs
similarity index 80%
rename from src/Responders/GuildUnloadedResponder.cs
rename to TeamOctolings.Octobot/Responders/GuildUnloadedResponder.cs
index 47bde75..c73c134 100644
--- a/src/Responders/GuildUnloadedResponder.cs
+++ b/TeamOctolings.Octobot/Responders/GuildUnloadedResponder.cs
@@ -1,18 +1,18 @@
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
-using Octobot.Data;
-using Octobot.Services;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.Gateway.Responders;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Services;
-namespace Octobot.Responders;
+namespace TeamOctolings.Octobot.Responders;
///
/// Handles removing guild ID from if the guild becomes unavailable.
///
[UsedImplicitly]
-public class GuildUnloadedResponder : IResponder
+public sealed class GuildUnloadedResponder : IResponder
{
private readonly GuildDataService _guildData;
private readonly ILogger _logger;
@@ -33,6 +33,6 @@ public class GuildUnloadedResponder : IResponder
_logger.LogInformation("Unloaded guild {GuildId}", guildId);
}
- return Task.FromResult(Result.FromSuccess());
+ return Task.FromResult(Result.Success);
}
}
diff --git a/src/Responders/MessageDeletedResponder.cs b/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs
similarity index 77%
rename from src/Responders/MessageDeletedResponder.cs
rename to TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs
index bfedb22..f0e3d22 100644
--- a/src/Responders/MessageDeletedResponder.cs
+++ b/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs
@@ -1,8 +1,5 @@
using System.Text;
using JetBrains.Annotations;
-using Octobot.Data;
-using Octobot.Extensions;
-using Octobot.Services;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
@@ -10,15 +7,18 @@ using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Discord.Gateway.Responders;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Services;
-namespace Octobot.Responders;
+namespace TeamOctolings.Octobot.Responders;
///
/// Handles logging the contents of a deleted message and the user who deleted the message
/// to a guild's if one is set.
///
[UsedImplicitly]
-public class MessageDeletedResponder : IResponder
+public sealed class MessageDeletedResponder : IResponder
{
private readonly IDiscordRestAuditLogAPI _auditLogApi;
private readonly IDiscordRestChannelAPI _channelApi;
@@ -39,37 +39,37 @@ public class MessageDeletedResponder : IResponder
{
if (!gatewayEvent.GuildID.IsDefined(out var guildId))
{
- return Result.FromSuccess();
+ return Result.Success;
}
var cfg = await _guildData.GetSettings(guildId, ct);
if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
{
- return Result.FromSuccess();
+ return Result.Success;
}
var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct);
if (!messageResult.IsDefined(out var message))
{
- return Result.FromError(messageResult);
+ return ResultExtensions.FromError(messageResult);
}
if (string.IsNullOrWhiteSpace(message.Content))
{
- return Result.FromSuccess();
+ return Result.Success;
}
var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync(
guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct);
if (!auditLogResult.IsDefined(out var auditLogPage))
{
- return Result.FromError(auditLogResult);
+ return ResultExtensions.FromError(auditLogResult);
}
- var auditLog = auditLogPage.AuditLogEntries.Single();
-
var deleterResult = Result.FromSuccess(message.Author);
- if (auditLog.UserID is not null
+
+ var auditLog = auditLogPage.AuditLogEntries.SingleOrDefault();
+ if (auditLog is { UserID: not null }
&& auditLog.Options.Value.ChannelID == gatewayEvent.ChannelID
&& DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2)
{
@@ -78,15 +78,16 @@ public class MessageDeletedResponder : IResponder
if (!deleterResult.IsDefined(out var deleter))
{
- return Result.FromError(deleterResult);
+ return ResultExtensions.FromError(deleterResult);
}
Messages.Culture = GuildSettings.Language.Get(cfg);
- var builder = new StringBuilder().AppendLine(
- string.Format(Messages.DescriptionActionJumpToChannel,
- Mention.Channel(gatewayEvent.ChannelID)))
- .AppendLine(message.Content.InBlockCode());
+ var builder = new StringBuilder()
+ .AppendLine(message.Content.InBlockCode())
+ .AppendLine(
+ string.Format(Messages.DescriptionActionJumpToChannel, Mention.Channel(gatewayEvent.ChannelID))
+ );
var embed = new EmbedBuilder()
.WithSmallTitle(
@@ -101,6 +102,6 @@ public class MessageDeletedResponder : IResponder
return await _channelApi.CreateMessageWithEmbedResultAsync(
GuildSettings.PrivateFeedbackChannel.Get(cfg), embedResult: embed,
- allowedMentions: Octobot.NoMentions, ct: ct);
+ allowedMentions: Utility.NoMentions, ct: ct);
}
}
diff --git a/src/Responders/MessageEditedResponder.cs b/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs
similarity index 73%
rename from src/Responders/MessageEditedResponder.cs
rename to TeamOctolings.Octobot/Responders/MessageEditedResponder.cs
index c7426d2..2968562 100644
--- a/src/Responders/MessageEditedResponder.cs
+++ b/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs
@@ -1,9 +1,6 @@
using System.Text;
using DiffPlex.DiffBuilder;
using JetBrains.Annotations;
-using Octobot.Data;
-using Octobot.Extensions;
-using Octobot.Services;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
@@ -12,15 +9,18 @@ using Remora.Discord.Caching.Services;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Gateway.Responders;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+using TeamOctolings.Octobot.Services;
-namespace Octobot.Responders;
+namespace TeamOctolings.Octobot.Responders;
///
/// Handles logging the difference between an edited message's old and new content
/// to a guild's if one is set.
///
[UsedImplicitly]
-public class MessageEditedResponder : IResponder
+public sealed class MessageEditedResponder : IResponder
{
private readonly CacheService _cacheService;
private readonly IDiscordRestChannelAPI _channelApi;
@@ -46,30 +46,18 @@ public class MessageEditedResponder : IResponder
return new ArgumentNullError(nameof(gatewayEvent.ChannelID));
}
- if (!gatewayEvent.GuildID.IsDefined(out var guildId))
+ if (!gatewayEvent.GuildID.IsDefined(out var guildId)
+ || !gatewayEvent.Author.IsDefined(out var author)
+ || !gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)
+ || !gatewayEvent.Content.IsDefined(out var newContent))
{
- return Result.FromSuccess();
- }
-
- if (gatewayEvent.Author.IsDefined(out var author) && author.IsBot.OrDefault(false))
- {
- return Result.FromSuccess();
- }
-
- if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp))
- {
- return Result.FromSuccess(); // The message wasn't actually edited
- }
-
- if (!gatewayEvent.Content.IsDefined(out var newContent))
- {
- return Result.FromSuccess();
+ return Result.Success;
}
var cfg = await _guildData.GetSettings(guildId, ct);
- if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
+ if (author.IsBot.OrDefault(false) || GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty())
{
- return Result.FromSuccess();
+ return Result.Success;
}
var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId);
@@ -78,12 +66,12 @@ public class MessageEditedResponder : IResponder
if (!messageResult.IsDefined(out var message))
{
_ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct);
- return Result.FromSuccess();
+ return Result.Success;
}
if (message.Content == newContent)
{
- return Result.FromSuccess();
+ return Result.Success;
}
// Custom event responders are called earlier than responders responsible for message caching
@@ -101,10 +89,11 @@ public class MessageEditedResponder : IResponder
Messages.Culture = GuildSettings.Language.Get(cfg);
- var builder = new StringBuilder().AppendLine(
- string.Format(Messages.DescriptionActionJumpToMessage,
- $"https://discord.com/channels/{guildId}/{channelId}/{messageId}"))
- .AppendLine(diff.AsMarkdown());
+ var builder = new StringBuilder()
+ .AppendLine(diff.AsMarkdown())
+ .AppendLine(string.Format(Messages.DescriptionActionJumpToMessage,
+ $"https://discord.com/channels/{guildId}/{channelId}/{messageId}")
+ );
var embed = new EmbedBuilder()
.WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author)
@@ -115,6 +104,6 @@ public class MessageEditedResponder : IResponder
return await _channelApi.CreateMessageWithEmbedResultAsync(
GuildSettings.PrivateFeedbackChannel.Get(cfg), embedResult: embed,
- allowedMentions: Octobot.NoMentions, ct: ct);
+ allowedMentions: Utility.NoMentions, ct: ct);
}
}
diff --git a/src/Responders/MessageReceivedResponder.cs b/TeamOctolings.Octobot/Responders/MessageReceivedResponder.cs
similarity index 87%
rename from src/Responders/MessageReceivedResponder.cs
rename to TeamOctolings.Octobot/Responders/MessageReceivedResponder.cs
index 6ab7199..24d53a5 100644
--- a/src/Responders/MessageReceivedResponder.cs
+++ b/TeamOctolings.Octobot/Responders/MessageReceivedResponder.cs
@@ -5,13 +5,13 @@ using Remora.Discord.Gateway.Responders;
using Remora.Rest.Core;
using Remora.Results;
-namespace Octobot.Responders;
+namespace TeamOctolings.Octobot.Responders;
///
/// Handles sending replies to easter egg messages.
///
[UsedImplicitly]
-public class MessageCreateResponder : IResponder
+public sealed class MessageCreateResponder : IResponder
{
private readonly IDiscordRestChannelAPI _channelApi;
@@ -34,6 +34,6 @@ public class MessageCreateResponder : IResponder
"лан" => "https://i.ibb.co/VYH2QLc/lan.jpg",
_ => default(Optional)
});
- return Task.FromResult(Result.FromSuccess());
+ return Task.FromResult(Result.Success);
}
}
diff --git a/TeamOctolings.Octobot/Services/AccessControlService.cs b/TeamOctolings.Octobot/Services/AccessControlService.cs
new file mode 100644
index 0000000..d39c9e5
--- /dev/null
+++ b/TeamOctolings.Octobot/Services/AccessControlService.cs
@@ -0,0 +1,142 @@
+using Remora.Discord.API.Abstractions.Objects;
+using Remora.Discord.API.Abstractions.Rest;
+using Remora.Rest.Core;
+using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
+
+namespace TeamOctolings.Octobot.Services;
+
+public sealed class AccessControlService
+{
+ private readonly GuildDataService _data;
+ private readonly IDiscordRestGuildAPI _guildApi;
+ private readonly IDiscordRestUserAPI _userApi;
+
+ public AccessControlService(GuildDataService data, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi)
+ {
+ _data = data;
+ _guildApi = guildApi;
+ _userApi = userApi;
+ }
+
+ private static bool CheckPermission(IEnumerable roles, GuildData data, MemberData memberData,
+ DiscordPermission permission)
+ {
+ var moderatorRole = GuildSettings.ModeratorRole.Get(data.Settings);
+ if (!moderatorRole.Empty() && memberData.Roles.Contains(moderatorRole.Value))
+ {
+ return true;
+ }
+
+ return roles
+ .Where(r => memberData.Roles.Contains(r.ID.Value))
+ .Any(r =>
+ r.Permissions.HasPermission(permission)
+ );
+ }
+
+ ///
+ /// Checks whether or not a member can interact with another member
+ ///
+ /// The ID of the guild in which an operation is being performed.
+ /// The executor of the operation.
+ /// The target of the operation.
+ /// The operation.
+ /// The cancellation token for this operation.
+ ///
+ ///
+ /// - A result which has succeeded with a null string if the member can interact with the target.
+ /// -
+ /// A result which has succeeded with a non-null string containing the error message if the member cannot
+ /// interact with the target.
+ ///
+ /// - A result which has failed if an error occurred during the execution of this method.
+ ///
+ ///
+ public async Task> CheckInteractionsAsync(
+ Snowflake guildId, Snowflake? interacterId, Snowflake targetId, string action, CancellationToken ct = default)
+ {
+ if (interacterId == targetId)
+ {
+ return Result.FromSuccess($"UserCannot{action}Themselves".Localized());
+ }
+
+ var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct);
+ if (!guildResult.IsDefined(out var guild))
+ {
+ return Result.FromError(guildResult);
+ }
+
+ if (interacterId == guild.OwnerID)
+ {
+ return Result.FromSuccess(null);
+ }
+
+ var botResult = await _userApi.GetCurrentUserAsync(ct);
+ if (!botResult.IsDefined(out var bot))
+ {
+ return Result.FromError(botResult);
+ }
+
+ var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct);
+ if (!rolesResult.IsDefined(out var roles))
+ {
+ return Result.FromError(rolesResult);
+ }
+
+ var data = await _data.GetData(guildId, ct);
+ var targetData = data.GetOrCreateMemberData(targetId);
+ var botData = data.GetOrCreateMemberData(bot.ID);
+
+ if (interacterId is null)
+ {
+ return CheckInteractions(action, guild, roles, targetData, botData, botData);
+ }
+
+ var interacterData = data.GetOrCreateMemberData(interacterId.Value);
+ var hasPermission = CheckPermission(roles, data, interacterData,
+ action switch
+ {
+ "Ban" => DiscordPermission.BanMembers,
+ "Kick" => DiscordPermission.KickMembers,
+ "Mute" or "Unmute" => DiscordPermission.ModerateMembers,
+ _ => throw new Exception()
+ });
+
+ return hasPermission
+ ? CheckInteractions(action, guild, roles, targetData, botData, interacterData)
+ : Result.FromSuccess($"UserCannot{action}Members".Localized());
+ }
+
+ private static Result CheckInteractions(
+ string action, IGuild guild, IReadOnlyList roles, MemberData targetData, MemberData botData,
+ MemberData interacterData)
+ {
+ if (botData.Id == targetData.Id)
+ {
+ return Result.FromSuccess($"UserCannot{action}Bot".Localized());
+ }
+
+ if (targetData.Id == guild.OwnerID)
+ {
+ return Result.FromSuccess($"UserCannot{action}Owner".Localized());
+ }
+
+ var targetRoles = roles.Where(r => targetData.Roles.Contains(r.ID.Value)).ToList();
+ var botRoles = roles.Where(r => botData.Roles.Contains(r.ID.Value));
+
+ var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.MaxOrDefault(r => r.Position);
+ if (targetBotRoleDiff >= 0)
+ {
+ return Result.FromSuccess($"BotCannot{action}Target".Localized());
+ }
+
+ var interacterRoles = roles.Where(r => interacterData.Roles.Contains(r.ID.Value));
+ var targetInteracterRoleDiff
+ = targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.MaxOrDefault(r => r.Position);
+ return targetInteracterRoleDiff < 0
+ ? Result.FromSuccess(null)
+ : Result.FromSuccess($"UserCannot{action}Target".Localized());
+ }
+}
diff --git a/src/Services/GuildDataService.cs b/TeamOctolings.Octobot/Services/GuildDataService.cs
similarity index 89%
rename from src/Services/GuildDataService.cs
rename to TeamOctolings.Octobot/Services/GuildDataService.cs
index c9458a0..a7af7c9 100644
--- a/src/Services/GuildDataService.cs
+++ b/TeamOctolings.Octobot/Services/GuildDataService.cs
@@ -3,10 +3,10 @@ using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-using Octobot.Data;
using Remora.Rest.Core;
+using TeamOctolings.Octobot.Data;
-namespace Octobot.Services;
+namespace TeamOctolings.Octobot.Services;
///
/// Handles saving, loading, initializing and providing .
@@ -27,7 +27,7 @@ public sealed class GuildDataService : BackgroundService
return SaveAsync(ct);
}
- private Task SaveAsync(CancellationToken ct)
+ private Task SaveAsync(CancellationToken ct = default)
{
var tasks = new List();
var datas = _datas.Values.ToArray();
@@ -44,7 +44,7 @@ public sealed class GuildDataService : BackgroundService
return Task.WhenAll(tasks);
}
- private static async Task SerializeObjectSafelyAsync(T obj, string path, CancellationToken ct)
+ private static async Task SerializeObjectSafelyAsync(T obj, string path, CancellationToken ct = default)
{
var tempFilePath = path + ".tmp";
await using (var tempFileStream = File.Create(tempFilePath))
@@ -78,7 +78,7 @@ public sealed class GuildDataService : BackgroundService
var settingsPath = $"{path}/Settings.json";
var scheduledEventsPath = $"{path}/ScheduledEvents.json";
- MigrateGuildData(guildId, path);
+ MigrateDataDirectory(guildId, path);
Directory.CreateDirectory(path);
@@ -106,6 +106,11 @@ public sealed class GuildDataService : BackgroundService
dataLoadFailed = true;
}
+ if (jsonSettings is not null)
+ {
+ FixJsonSettings(jsonSettings);
+ }
+
await using var eventsStream = File.OpenRead(scheduledEventsPath);
Dictionary? events = null;
try
@@ -155,7 +160,7 @@ public sealed class GuildDataService : BackgroundService
return finalData;
}
- private void MigrateGuildData(Snowflake guildId, string newPath)
+ private void MigrateDataDirectory(Snowflake guildId, string newPath)
{
var oldPath = $"{guildId}";
@@ -169,6 +174,15 @@ public sealed class GuildDataService : BackgroundService
}
}
+ private static void FixJsonSettings(JsonNode settings)
+ {
+ var language = settings[GuildSettings.Language.Name]?.GetValue();
+ if (language is "mctaylors-ru")
+ {
+ settings[GuildSettings.Language.Name] = "ru";
+ }
+ }
+
public async Task GetSettings(Snowflake guildId, CancellationToken ct = default)
{
return (await GetData(guildId, ct)).Settings;
diff --git a/src/Services/Update/MemberUpdateService.cs b/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs
similarity index 84%
rename from src/Services/Update/MemberUpdateService.cs
rename to TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs
index 7674bbe..3170060 100644
--- a/src/Services/Update/MemberUpdateService.cs
+++ b/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs
@@ -2,16 +2,16 @@ using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-using Octobot.Data;
-using Octobot.Extensions;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
-namespace Octobot.Services.Update;
+namespace TeamOctolings.Octobot.Services.Update;
public sealed partial class MemberUpdateService : BackgroundService
{
@@ -26,20 +26,20 @@ public sealed partial class MemberUpdateService : BackgroundService
"Torus", "Violet", "Vortex", "Vulture", "Wagon", "Whale", "Woodpecker", "Zebra", "Zigzag"
];
+ private readonly AccessControlService _access;
private readonly IDiscordRestChannelAPI _channelApi;
private readonly IDiscordRestGuildAPI _guildApi;
private readonly GuildDataService _guildData;
private readonly ILogger _logger;
- private readonly Utility _utility;
- public MemberUpdateService(IDiscordRestChannelAPI channelApi, IDiscordRestGuildAPI guildApi,
- GuildDataService guildData, ILogger logger, Utility utility)
+ public MemberUpdateService(AccessControlService access, IDiscordRestChannelAPI channelApi,
+ IDiscordRestGuildAPI guildApi, GuildDataService guildData, ILogger logger)
{
+ _access = access;
_channelApi = channelApi;
_guildApi = guildApi;
_guildData = guildData;
_logger = logger;
- _utility = utility;
}
protected override async Task ExecuteAsync(CancellationToken ct)
@@ -62,7 +62,7 @@ public sealed partial class MemberUpdateService : BackgroundService
}
}
- private async Task TickMemberDatasAsync(Snowflake guildId, CancellationToken ct)
+ private async Task TickMemberDatasAsync(Snowflake guildId, CancellationToken ct = default)
{
var guildData = await _guildData.GetData(guildId, ct);
var defaultRole = GuildSettings.DefaultRole.Get(guildData.Settings);
@@ -79,7 +79,7 @@ public sealed partial class MemberUpdateService : BackgroundService
private async Task TickMemberDataAsync(Snowflake guildId, GuildData guildData, Snowflake defaultRole,
MemberData data,
- CancellationToken ct)
+ CancellationToken ct = default)
{
var failedResults = new List();
var id = data.Id.ToSnowflake();
@@ -94,10 +94,10 @@ public sealed partial class MemberUpdateService : BackgroundService
}
var interactionResult
- = await _utility.CheckInteractionsAsync(guildId, null, id, "Update", ct);
+ = await _access.CheckInteractionsAsync(guildId, null, id, "Update", ct);
if (!interactionResult.IsSuccess)
{
- return Result.FromError(interactionResult);
+ return ResultExtensions.FromError(interactionResult);
}
var canInteract = interactionResult.Entity is null;
@@ -121,7 +121,7 @@ public sealed partial class MemberUpdateService : BackgroundService
if (!canInteract)
{
- return Result.FromSuccess();
+ return Result.Success;
}
var autoUnmuteResult = await TryAutoUnmuteAsync(guildId, id, data, ct);
@@ -144,11 +144,18 @@ public sealed partial class MemberUpdateService : BackgroundService
}
private async Task TryAutoUnbanAsync(
- Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct)
+ Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct = default)
{
if (data.BannedUntil is null || DateTimeOffset.UtcNow <= data.BannedUntil)
{
- return Result.FromSuccess();
+ return Result.Success;
+ }
+
+ var existingBanResult = await _guildApi.GetGuildBanAsync(guildId, id, ct);
+ if (!existingBanResult.IsDefined())
+ {
+ data.BannedUntil = null;
+ return Result.Success;
}
var unbanResult = await _guildApi.RemoveGuildBanAsync(
@@ -162,11 +169,11 @@ public sealed partial class MemberUpdateService : BackgroundService
}
private async Task TryAutoUnmuteAsync(
- Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct)
+ Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct = default)
{
if (data.MutedUntil is null || DateTimeOffset.UtcNow <= data.MutedUntil)
{
- return Result.FromSuccess();
+ return Result.Success;
}
var unmuteResult = await _guildApi.ModifyGuildMemberAsync(
@@ -181,7 +188,7 @@ public sealed partial class MemberUpdateService : BackgroundService
}
private async Task FilterNicknameAsync(Snowflake guildId, IUser user, IGuildMember member,
- CancellationToken ct)
+ CancellationToken ct = default)
{
var currentNickname = member.Nickname.IsDefined(out var nickname)
? nickname
@@ -202,7 +209,7 @@ public sealed partial class MemberUpdateService : BackgroundService
if (!usernameChanged)
{
- return Result.FromSuccess();
+ return Result.Success;
}
var newNickname = string.Concat(characterList.ToArray());
@@ -219,16 +226,17 @@ public sealed partial class MemberUpdateService : BackgroundService
private static partial Regex IllegalChars();
private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData data, Snowflake guildId,
- CancellationToken ct)
+ CancellationToken ct = default)
{
if (DateTimeOffset.UtcNow < reminder.At)
{
- return Result.FromSuccess();
+ return Result.Success;
}
var builder = new StringBuilder()
- .AppendBulletPointLine(string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text)))
- .AppendBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}"));
+ .AppendLine(MarkdownExtensions.Quote(reminder.Text))
+ .AppendBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage,
+ $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}"));
var embed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.Reminder, user.GetTag()), user)
@@ -240,10 +248,10 @@ public sealed partial class MemberUpdateService : BackgroundService
reminder.ChannelId.ToSnowflake(), Mention.User(user), embedResult: embed, ct: ct);
if (!messageResult.IsSuccess)
{
- return messageResult;
+ return ResultExtensions.FromError(messageResult);
}
data.Reminders.Remove(reminder);
- return Result.FromSuccess();
+ return Result.Success;
}
}
diff --git a/src/Services/Update/ScheduledEventUpdateService.cs b/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs
similarity index 94%
rename from src/Services/Update/ScheduledEventUpdateService.cs
rename to TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs
index ac5c109..ef145aa 100644
--- a/src/Services/Update/ScheduledEventUpdateService.cs
+++ b/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs
@@ -1,8 +1,6 @@
using System.Text.Json.Nodes;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-using Octobot.Data;
-using Octobot.Extensions;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Objects;
@@ -10,8 +8,10 @@ using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
-namespace Octobot.Services.Update;
+namespace TeamOctolings.Octobot.Services.Update;
public sealed class ScheduledEventUpdateService : BackgroundService
{
@@ -46,14 +46,14 @@ public sealed class ScheduledEventUpdateService : BackgroundService
}
}
- private async Task TickScheduledEventsAsync(Snowflake guildId, CancellationToken ct)
+ private async Task TickScheduledEventsAsync(Snowflake guildId, CancellationToken ct = default)
{
var failedResults = new List();
var data = await _guildData.GetData(guildId, ct);
var eventsResult = await _eventApi.ListScheduledEventsForGuildAsync(guildId, ct: ct);
if (!eventsResult.IsDefined(out var events))
{
- return Result.FromError(eventsResult);
+ return ResultExtensions.FromError(eventsResult);
}
SyncScheduledEvents(data, events);
@@ -133,7 +133,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
private async Task TickScheduledEventAsync(
Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData,
- CancellationToken ct)
+ CancellationToken ct = default)
{
if (GuildSettings.AutoStartEvents.Get(data.Settings)
&& DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime
@@ -147,7 +147,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
|| eventData.EarlyNotificationSent
|| DateTimeOffset.UtcNow < scheduledEvent.ScheduledStartTime - offset)
{
- return Result.FromSuccess();
+ return Result.Success;
}
var sendResult = await SendEarlyEventNotificationAsync(scheduledEvent, data, ct);
@@ -160,7 +160,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
}
private async Task AutoStartEventAsync(
- Snowflake guildId, IGuildScheduledEvent scheduledEvent, CancellationToken ct)
+ Snowflake guildId, IGuildScheduledEvent scheduledEvent, CancellationToken ct = default)
{
return (Result)await _eventApi.ModifyGuildScheduledEventAsync(
guildId, scheduledEvent.ID,
@@ -182,7 +182,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
{
if (GuildSettings.EventNotificationChannel.Get(settings).Empty())
{
- return Result.FromSuccess();
+ return Result.Success;
}
if (!scheduledEvent.Creator.IsDefined(out var creator))
@@ -204,7 +204,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
if (!embedDescriptionResult.IsDefined(out var embedDescription))
{
- return Result.FromError(embedDescriptionResult);
+ return ResultExtensions.FromError(embedDescriptionResult);
}
var embed = new EmbedBuilder()
@@ -223,7 +223,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
var button = new ButtonComponent(
ButtonComponentStyle.Link,
Messages.ButtonOpenEventInfo,
- new PartialEmoji(Name: "📋"),
+ new PartialEmoji(Name: "\ud83d\udccb"), // 'CLIPBOARD' (U+1F4CB)
URL: $"https://discord.com/events/{scheduledEvent.GuildID}/{scheduledEvent.ID}"
);
@@ -283,7 +283,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
{
- return Result.FromSuccess();
+ return Result.Success;
}
var embedDescriptionResult = scheduledEvent.EntityType switch
@@ -298,12 +298,12 @@ public sealed class ScheduledEventUpdateService : BackgroundService
scheduledEvent, data, ct);
if (!contentResult.IsDefined(out var content))
{
- return Result.FromError(contentResult);
+ return ResultExtensions.FromError(contentResult);
}
if (!embedDescriptionResult.IsDefined(out var embedDescription))
{
- return Result.FromError(embedDescriptionResult);
+ return ResultExtensions.FromError(embedDescriptionResult);
}
var startedEmbed = new EmbedBuilder()
@@ -319,12 +319,12 @@ public sealed class ScheduledEventUpdateService : BackgroundService
}
private async Task SendScheduledEventCompletedMessage(ScheduledEventData eventData, GuildData data,
- CancellationToken ct)
+ CancellationToken ct = default)
{
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
{
data.ScheduledEvents.Remove(eventData.Id);
- return Result.FromSuccess();
+ return Result.Success;
}
var completedEmbed = new EmbedBuilder()
@@ -351,12 +351,12 @@ public sealed class ScheduledEventUpdateService : BackgroundService
}
private async Task SendScheduledEventCancelledMessage(ScheduledEventData eventData, GuildData data,
- CancellationToken ct)
+ CancellationToken ct = default)
{
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
{
data.ScheduledEvents.Remove(eventData.Id);
- return Result.FromSuccess();
+ return Result.Success;
}
var embed = new EmbedBuilder()
@@ -405,18 +405,18 @@ public sealed class ScheduledEventUpdateService : BackgroundService
}
private async Task SendEarlyEventNotificationAsync(
- IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct)
+ IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default)
{
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
{
- return Result.FromSuccess();
+ return Result.Success;
}
var contentResult = await _utility.GetEventNotificationMentions(
scheduledEvent, data, ct);
if (!contentResult.IsDefined(out var content))
{
- return Result.FromError(contentResult);
+ return ResultExtensions.FromError(contentResult);
}
var earlyResult = new EmbedBuilder()
diff --git a/src/Services/Update/SongUpdateService.cs b/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs
similarity index 79%
rename from src/Services/Update/SongUpdateService.cs
rename to TeamOctolings.Octobot/Services/Update/SongUpdateService.cs
index 5d4a337..d0b46ae 100644
--- a/src/Services/Update/SongUpdateService.cs
+++ b/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs
@@ -4,7 +4,7 @@ using Remora.Discord.API.Gateway.Commands;
using Remora.Discord.API.Objects;
using Remora.Discord.Gateway;
-namespace Octobot.Services.Update;
+namespace TeamOctolings.Octobot.Services.Update;
public sealed class SongUpdateService : BackgroundService
{
@@ -36,6 +36,11 @@ public sealed class SongUpdateService : BackgroundService
("Deep Cut", "Fins in the Air", new TimeSpan(0, 3, 1))
];
+ private static readonly (string Author, string Name, TimeSpan Duration)[] SpecialSongList =
+ [
+ ("Squid Sisters", "Maritime Memory", new TimeSpan(0, 2, 47))
+ ];
+
private readonly List _activityList = [new Activity("with Remora.Discord", ActivityType.Game)];
private readonly DiscordGatewayClient _client;
@@ -58,19 +63,33 @@ public sealed class SongUpdateService : BackgroundService
while (!ct.IsCancellationRequested)
{
- var nextSong = SongList[_nextSongIndex];
+ var nextSong = NextSong();
_activityList[0] = new Activity($"{nextSong.Name} / {nextSong.Author}",
ActivityType.Listening);
_client.SubmitCommand(
new UpdatePresence(
UserStatus.Online, false, DateTimeOffset.UtcNow, _activityList));
- _nextSongIndex++;
- if (_nextSongIndex >= SongList.Length)
- {
- _nextSongIndex = 0;
- }
await Task.Delay(nextSong.Duration, ct);
}
}
+
+ private (string Author, string Name, TimeSpan Duration) NextSong()
+ {
+ var today = DateTime.Today;
+ // Discontinuation of Online Services for Nintendo Wii U
+ if (today.Day is 8 or 9 && today.Month is 4)
+ {
+ return SpecialSongList[0]; // Maritime Memory / Squid Sisters
+ }
+
+ var nextSong = SongList[_nextSongIndex];
+ _nextSongIndex++;
+ if (_nextSongIndex >= SongList.Length)
+ {
+ _nextSongIndex = 0;
+ }
+
+ return nextSong;
+ }
}
diff --git a/Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj
similarity index 83%
rename from Octobot.csproj
rename to TeamOctolings.Octobot/TeamOctolings.Octobot.csproj
index ab76400..19e37f9 100644
--- a/Octobot.csproj
+++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj
@@ -1,4 +1,4 @@
-
+
Exe
@@ -16,29 +16,31 @@
TeamOctolings
en
A general-purpose Discord bot for moderation written in C#
- docs/octobot.ico
+ ../docs/octobot.ico
+ false
+
-
-
-
-
+
+
+
+
-
+
ResXFileCodeGenerator
Messages.Designer.cs
-
+
diff --git a/src/Services/Utility.cs b/TeamOctolings.Octobot/Utility.cs
similarity index 52%
rename from src/Services/Utility.cs
rename to TeamOctolings.Octobot/Utility.cs
index ad06315..f337d93 100644
--- a/src/Services/Utility.cs
+++ b/TeamOctolings.Octobot/Utility.cs
@@ -1,16 +1,19 @@
using System.Drawing;
using System.Text;
using System.Text.Json.Nodes;
-using Octobot.Data;
-using Octobot.Extensions;
+using Microsoft.Extensions.Logging;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
+using Remora.Discord.API.Objects;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Extensions.Formatting;
using Remora.Rest.Core;
using Remora.Results;
+using TeamOctolings.Octobot.Attributes;
+using TeamOctolings.Octobot.Data;
+using TeamOctolings.Octobot.Extensions;
-namespace Octobot.Services;
+namespace TeamOctolings.Octobot;
///
/// Provides utility methods that cannot be transformed to extension methods because they require usage
@@ -18,133 +21,23 @@ namespace Octobot.Services;
///
public sealed class Utility
{
+ public static readonly AllowedMentions NoMentions = new(
+ Array.Empty(), Array.Empty(), Array.Empty());
+
private readonly IDiscordRestChannelAPI _channelApi;
private readonly IDiscordRestGuildScheduledEventAPI _eventApi;
private readonly IDiscordRestGuildAPI _guildApi;
- private readonly IDiscordRestUserAPI _userApi;
public Utility(
- IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi,
- IDiscordRestUserAPI userApi)
+ IDiscordRestChannelAPI channelApi, IDiscordRestGuildScheduledEventAPI eventApi, IDiscordRestGuildAPI guildApi)
{
_channelApi = channelApi;
_eventApi = eventApi;
_guildApi = guildApi;
- _userApi = userApi;
}
- ///
- /// Checks whether or not a member can interact with another member
- ///
- /// The ID of the guild in which an operation is being performed.
- /// The executor of the operation.
- /// The target of the operation.
- /// The operation.
- /// The cancellation token for this operation.
- ///
- ///
- /// - A result which has succeeded with a null string if the member can interact with the target.
- /// -
- /// A result which has succeeded with a non-null string containing the error message if the member cannot
- /// interact with the target.
- ///
- /// - A result which has failed if an error occurred during the execution of this method.
- ///
- ///
- public async Task> CheckInteractionsAsync(
- Snowflake guildId, Snowflake? interacterId, Snowflake targetId, string action, CancellationToken ct = default)
- {
- if (interacterId == targetId)
- {
- return Result.FromSuccess($"UserCannot{action}Themselves".Localized());
- }
-
- var botResult = await _userApi.GetCurrentUserAsync(ct);
- if (!botResult.IsDefined(out var bot))
- {
- return Result.FromError(botResult);
- }
-
- var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct);
- if (!guildResult.IsDefined(out var guild))
- {
- return Result.FromError(guildResult);
- }
-
- var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct);
- if (!targetMemberResult.IsDefined(out var targetMember))
- {
- return Result.FromSuccess(null);
- }
-
- var currentMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct);
- if (!currentMemberResult.IsDefined(out var currentMember))
- {
- return Result.FromError(currentMemberResult);
- }
-
- var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct);
- if (!rolesResult.IsDefined(out var roles))
- {
- return Result.FromError(rolesResult);
- }
-
- if (interacterId is null)
- {
- return CheckInteractions(action, guild, roles, targetMember, currentMember, currentMember);
- }
-
- var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId.Value, ct);
- return interacterResult.IsDefined(out var interacter)
- ? CheckInteractions(action, guild, roles, targetMember, currentMember, interacter)
- : Result.FromError(interacterResult);
- }
-
- private static Result CheckInteractions(
- string action, IGuild guild, IReadOnlyList roles, IGuildMember targetMember, IGuildMember currentMember,
- IGuildMember interacter)
- {
- if (!targetMember.User.IsDefined(out var targetUser))
- {
- return new ArgumentNullError(nameof(targetMember.User));
- }
-
- if (!interacter.User.IsDefined(out var interacterUser))
- {
- return new ArgumentNullError(nameof(interacter.User));
- }
-
- if (currentMember.User == targetMember.User)
- {
- return Result.FromSuccess($"UserCannot{action}Bot".Localized());
- }
-
- if (targetUser.ID == guild.OwnerID)
- {
- return Result.FromSuccess($"UserCannot{action}Owner".Localized());
- }
-
- var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)).ToList();
- var botRoles = roles.Where(r => currentMember.Roles.Contains(r.ID));
-
- var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.MaxOrDefault(r => r.Position);
- if (targetBotRoleDiff >= 0)
- {
- return Result.FromSuccess($"BotCannot{action}Target".Localized());
- }
-
- if (interacterUser.ID == guild.OwnerID)
- {
- return Result.FromSuccess(null);
- }
-
- var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID));
- var targetInteracterRoleDiff
- = targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.MaxOrDefault(r => r.Position);
- return targetInteracterRoleDiff < 0
- ? Result.FromSuccess(null)
- : Result.FromSuccess($"UserCannot{action}Target".Localized());
- }
+ [StaticCallersOnly]
+ public static ILogger? StaticLogger { get; set; }
///
/// Gets the string mentioning the and event subscribers related to
@@ -232,7 +125,7 @@ public sealed class Utility
}
}
- public async Task> GetEmergencyFeedbackChannel(IGuild guild, GuildData data, CancellationToken ct)
+ public async Task> GetEmergencyFeedbackChannel(IGuild guild, GuildData data, CancellationToken ct = default)
{
var privateFeedback = GuildSettings.PrivateFeedbackChannel.Get(data.Settings);
if (!privateFeedback.Empty())
diff --git a/docs/README.md b/docs/README.md
index 7056857..ccc3b83 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -15,23 +15,16 @@ Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) wr
* Reminding everyone about that new event you made
* Renaming those annoying self-hoisting members
* Log everything from joining the server to deleting messages
-* Listen to music!
+* Listen to Inkantation!
*...a-a-and more!*
## Building Octobot
-1. Install [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
-2. Go to the [Discord Developer Portal](https://discord.com/developers), create a new application and get a bot token. Don't forget to also enable all intents!
-3. Clone this repository and open `Octobot` folder.
-```
-git clone https://github.com/TeamOctolings/Octobot
-cd Octobot
-```
-4. Run Octobot using `dotnet` with `BOT_TOKEN` variable.
-```
-dotnet run BOT_TOKEN='ENTER_TOKEN_HERE'
-```
+Check out the Octobot's Wiki for details.
+
+| [Windows](https://github.com/TeamOctolings/Octobot/wiki/Installing-Windows) | [Linux/macOS](https://github.com/TeamOctolings/Octobot/wiki/Installing-Unix) |
+| --- | --- |
## Contributing
diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx
deleted file mode 100644
index f0b80a7..0000000
--- a/locale/Messages.tt-ru.resx
+++ /dev/null
@@ -1,657 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- text/microsoft-resx
-
-
- 2.0
-
-
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- я родился!
-
-
- сообщение {0} вырезано:
-
-
- сообщение {0} переделано:
-
-
- {0}, добро пожаловать на сервер {1}
-
-
- вииимо!
-
-
- вуууми!
-
-
- нгьес!
-
-
- вы были забанены
-
-
- время бана закончиловсь
-
-
- вы были кикнуты
-
-
- мс
-
-
- *тут ничего нет*
-
-
- нъет
-
-
- язык
-
-
- префикс
-
-
- удалять звание при муте
-
-
- разглашать о том что пришел новый шизоид
-
-
- звание замученного
-
-
- такого языка нету...
-
-
- да
-
-
- нъет
-
-
- шизик не забанен
-
-
- шизоид не замучен!
-
-
- здравствуйте (типо настройка)
-
-
- {0} забанен
-
-
- получать инфу о старте бота
-
-
- криво настроил прикол, давай по новой
-
-
- ты шо, мутить больше чем на 28 дней таймаут не разрешает, вот настроишь роль мута, тогда поговорим
-
-
- я не могу замутить ботов, сделай что нибудь
-
-
- роль для уведомлений о создании движухи
-
-
- канал для уведомлений о движухах
-
-
- получатели уведомлений о начале движух
-
-
- движуха "{0}" начинается
-
-
- движуха "{0}" отменена!
-
-
- движуха "{0}" завершена!
-
-
- вырезано {0} забавных сообщений
-
-
- ты все сломал! значение прикола `{0}` и так {1}
-
-
- нъет
-
-
- укажи самого шизика
-
-
- бан
-
-
- тебе нельзя иметь власть над сообщениями шизоидов
-
-
- кик шизиков нельзя
-
-
- тебе нельзя управлять шизоидами
-
-
- тебе нельзя редактировать дурку
-
-
- я не могу ваще никого банить чел.
-
-
- я не могу исправлять орфографический кринж участников, сделай что нибудь.
-
-
- я не могу ваще никого кикать чел.
-
-
- я не могу контроллировать за всеми ними, сделай что нибудь.
-
-
- я не могу этому серверу хоть че либо нибудь изменить, сделай что нибудь.
-
-
- ээбля френдли фаер огонь по своим
-
-
- бан админу нельзя
-
-
- бан этому шизику нельзя
-
-
- самобан нельзя
-
-
- я не могу его забанить...
-
-
- кик админу нельзя
-
-
- самокик нельзя
-
-
- ээбля френдли фаер огонь по своим
-
-
- я не могу его кикнуть...
-
-
- кик этому шизику нельзя
-
-
- мут админу нельзя
-
-
- самомут нельзя
-
-
- ээбля френдли фаер огонь по своим
-
-
- я не могу его замутить...
-
-
- мут этому шизику нельзя
-
-
- сильно
-
-
- ты замучен.
-
-
- ...
-
-
- тебе нельзя раззамучивать
-
-
- я не могу его раззамутить...
-
-
- движуха "{0}" начнется {1}!
-
-
- заранее пнуть в минутах до начала движухи
-
-
- у нас такого шизоида нету, проверь, валиден ли ID уважаемого (я забываю о шизоидах если они ливнули минимум месяц назад)
-
-
- дефолтное звание
-
-
- канал для секретных уведомлений
-
-
- канал для не секретных уведомлений
-
-
- вернуть звания при переподключении в дурку
-
-
- автоматом стартить движухи
-
-
- ответственный
-
-
- {0} создает новое событие:
-
-
- движуха произойдет {0} в канале {1}
-
-
- движуха будет происходить с {0} до {1} в {2}
-
-
- открыть ивент
-
-
- все это длилось `{0}`
-
-
- движуха происходит в {0}
-
-
- движуха происходит в {0} до {1}
-
-
- этот шизоид уже лежит в бане
-
-
- {0} раззабанен
-
-
- {0} в муте
-
-
- {0} в размуте
-
-
- этого шизоида никто не мутил.
-
-
- у нас такого шизоида нету...
-
-
- {0} вышел с посторонней помощью
-
-
- причина: {0}
-
-
- до: {0}
-
-
- этот шизоид УЖЕ замучился
-
-
- от {0}
-
-
- девелоперы:
-
-
- репа Octobot (тык)
-
-
- немного об {0}
-
-
- скучный девелопер + дизайнер создавший Octobot's Wiki
-
-
- ВАЖНЫЙ соучастник кодинг-стримов @Octol1ttle
-
-
- САМЫЙ ВАЖНЫЙ чел написавший кода больше всех (99.99%)
-
-
- напоминалка для {0} скрафченА
-
-
- напоминалка для {0}
-
-
- ты хотел чтоб я напомнил тебе {0}
-
-
- приколы Octobot
-
-
- прикол редактирован
-
-
- прикол сдох
-
-
- стало
-
-
- переобувать шизоидов пытающихся поднять себя в табе
-
-
- это страница
-
-
- если я был бы html, я бы сказал 404
-
-
- ну а если быть точнее, тут всего {0} страниц(-ы)
-
-
- следующее
-
-
- предыдущее
-
-
- напоминалки {0}
-
-
- у тебя нет напоминалки на этом номере!
-
-
- напоминалка уничтожена
-
-
- ты еще не крафтил напоминалки
-
-
- {0} откачен к заводским
-
-
- откатываемся к заводским...
-
-
- чекнуть сообщение: {0}
-
-
- чекнуть канал: {0}
-
-
- номер в списке: {0}
-
-
- время отправки: {0}
-
-
- че там в напоминалке: {0}
-
-
- дисплейнейм
-
-
- деанон {0}
-
-
- замучен
-
-
- юзер Discord со времен
-
-
- забанен
-
-
- приколы полученные по заслугам
-
-
- пермабан
-
-
- вышел из сервера
-
-
- замучен таймаутом
-
-
- замучен ролькой
-
-
- участник сервера со времен
-
-
- сервернейм
-
-
- рольки
-
-
- бустит сервер со времен
-
-
- рандомное число {0}:
-
-
- ну чувак...
-
-
- наибольшее: {0}
-
-
- наименьшее: {0}
-
-
- (дефолт)
-
-
- таймштамп для {0}:
-
-
- офсет: {0}
-
-
- дескрипшон гильдии
-
-
- создался
-
-
- админ гильдии
-
-
- буст гильдии
-
-
- уровень
-
-
- кол-во бустов
-
-
- алло а чё мне удалять-то
-
-
- вырезано {0} забавных сообщений от {1}
-
-
- произошёл тотальный разнос в гилддате.
-
-
- возможно всё съедет с крыши, но знай, что я больше ничё не сохраню.
-
-
- произошёл тотальный разнос в команде, удачи.
-
-
- если ты это читаешь второй раз за сегодня, пиши разрабам
-
-
- зарепортить баг
-
-
- ты там правильно напиши таймспан
-
-
- кикнут
-
-
- напоминалка подправлена
-
-
- абсолютли
-
-
- заявлено
-
-
- ваще не сомневайся
-
-
- 100% да
-
-
- будь в этом уверен
-
-
- я считаю что да
-
-
- ну вполне вероятно
-
-
- ну выглядит нормально
-
-
- мне сказали ок
-
-
- мгм
-
-
- ну-ка попробуй снова
-
-
- давай позже
-
-
- щас пока не скажу
-
-
- я не могу сейчас предсказать
-
-
- ну сконцентрируйся и давай еще раз
-
-
- даже не думай
-
-
- мое завление это нет
-
-
- я тут посчитал, короче нет
-
-
- выглядит такое себе
-
-
- чот сомневаюсь
-
-