diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index b961b81..57eea90 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -36,6 +36,5 @@ updates:
- "Remora.Discord.*"
# For all packages, ignore all patch updates
ignore:
- - dependency-name: "GitInfo"
- dependency-name: "*"
update-types: [ "version-update:semver-patch" ]
diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml
index 07d5b90..c92386e 100644
--- a/.github/workflows/build-pr.yml
+++ b/.github/workflows/build-pr.yml
@@ -22,13 +22,8 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
- with:
- dotnet-version: '9.0.x'
-
- name: ReSharper CLI InspectCode
- uses: muno92/resharper_inspectcode@1.13.0
+ uses: muno92/resharper_inspectcode@1.11.10
with:
solutionPath: ./Octobot.sln
ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor
diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml
index e7afe8e..a757eb2 100644
--- a/.github/workflows/build-push.yml
+++ b/.github/workflows/build-push.yml
@@ -5,83 +5,60 @@ concurrency:
on:
push:
- branches: [ "master", "deploy-test" ]
+ branches: [ "master" ]
jobs:
- upload-image:
- name: Upload Octobot Docker image
+ upload-solution:
+ name: Upload Octobot to production
runs-on: ubuntu-latest
permissions:
- packages: write
+ actions: read
+ contents: read
environment: production
steps:
- - name: Login to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Checkout repository
+ uses: actions/checkout@v4
- - name: Build and push Docker image
- uses: docker/build-push-action@v6
- with:
- push: true
- tags: ghcr.io/${{vars.NAMESPACE}}/${{vars.IMAGE_NAME}}:latest
- build-args: |
- BUILDKIT_CONTEXT_KEEP_GIT_DIR=1
- PUBLISH_OPTIONS=${{vars.PUBLISH_OPTIONS}}
+ - name: Publish solution
+ run: dotnet publish $PUBLISH_FLAGS
+ env:
+ PUBLISH_FLAGS: ${{vars.PUBLISH_FLAGS}}
- update-production:
- name: Update Octobot on production
- runs-on: ubuntu-latest
- environment: production
- needs: upload-image
-
- steps:
- - name: Copy SSH key
+ - name: Setup SSH key
run: |
- install -m 600 -D /dev/null ~/.ssh/id_ed25519
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
+ install -m 600 -D /dev/null ~/.ssh/id_rsa
+ echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
+ ssh-keyscan -H $SSH_HOST > ~/.ssh/known_hosts
shell: bash
env:
SSH_PRIVATE_KEY: ${{secrets.SSH_PRIVATE_KEY}}
-
- - name: Generate SSH known hosts file
- run: |
- ssh-keyscan -H -p $SSH_PORT $SSH_HOST > ~/.ssh/known_hosts
- shell: bash
- env:
SSH_HOST: ${{secrets.SSH_HOST}}
- SSH_PORT: ${{secrets.SSH_PORT}}
-
+
- name: Stop currently running instance
run: |
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST $STOP_COMMAND
+ ssh $SSH_USER@$SSH_HOST $STOP_COMMAND
shell: bash
env:
- SSH_PORT: ${{secrets.SSH_PORT}}
SSH_USER: ${{secrets.SSH_USER}}
SSH_HOST: ${{secrets.SSH_HOST}}
STOP_COMMAND: ${{vars.STOP_COMMAND}}
- - name: Update Docker image
+ - name: Upload published solution
run: |
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST docker pull ghcr.io/$NAMESPACE/$IMAGE_NAME:latest
+ scp -r $UPLOAD_FROM $SSH_USER@$SSH_HOST:$UPLOAD_TO
shell: bash
env:
- SSH_PORT: ${{secrets.SSH_PORT}}
SSH_USER: ${{secrets.SSH_USER}}
SSH_HOST: ${{secrets.SSH_HOST}}
- NAMESPACE: ${{vars.NAMESPACE}}
- IMAGE_NAME: ${{vars.IMAGE_NAME}}
+ UPLOAD_FROM: ${{vars.UPLOAD_FROM}}
+ UPLOAD_TO: ${{vars.UPLOAD_TO}}
- name: Start new instance
run: |
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST $START_COMMAND
+ ssh $SSH_USER@$SSH_HOST $START_COMMAND
shell: bash
env:
- SSH_PORT: ${{secrets.SSH_PORT}}
SSH_USER: ${{secrets.SSH_USER}}
SSH_HOST: ${{secrets.SSH_HOST}}
START_COMMAND: ${{vars.START_COMMAND}}
diff --git a/.gitignore b/.gitignore
index fcda727..f97f6b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,3 @@ riderModule.iml
/.vs/
GuildData/
Logs/
-compose.yaml
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 6cfeac6..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,15 +0,0 @@
-FROM mcr.microsoft.com/dotnet/sdk:9.0@sha256:7d24e90a392e88eb56093e4eb325ff883ad609382a55d42f17fd557b997022ca AS build-env
-WORKDIR /Octobot
-
-# Copy everything
-COPY . ./
-# Load build argument with publish options
-ARG PUBLISH_OPTIONS="-c Release"
-# Build and publish a release
-RUN dotnet publish ./TeamOctolings.Octobot $PUBLISH_OPTIONS -o out
-
-# Build runtime image
-FROM mcr.microsoft.com/dotnet/runtime:9.0@sha256:1e5eb0ed94ca96a34a914456db80e48bd1bb7bc3e3c8eda5e2c3d89c153c3081
-WORKDIR /Octobot
-COPY --from=build-env /Octobot/out .
-ENTRYPOINT ["./TeamOctolings.Octobot"]
diff --git a/TeamOctolings.Octobot/BuildInfo.cs b/TeamOctolings.Octobot/BuildInfo.cs
index a91e7f3..4b9a09f 100644
--- a/TeamOctolings.Octobot/BuildInfo.cs
+++ b/TeamOctolings.Octobot/BuildInfo.cs
@@ -2,9 +2,7 @@
public static class BuildInfo
{
- public const string WebsiteUrl = "https://teamoctolings.github.io/Octobot";
-
- private const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot";
+ public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot";
public const string IssuesUrl = $"{RepositoryUrl}/issues";
diff --git a/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs
index 28caccf..dbb8b12 100644
--- a/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs
@@ -106,9 +106,9 @@ public sealed class AboutCommandGroup : CommandGroup
var repositoryButton = new ButtonComponent(
ButtonComponentStyle.Link,
- Messages.ButtonOpenWebsite,
+ Messages.ButtonOpenRepository,
new PartialEmoji(Name: "\ud83c\udf10"), // 'GLOBE WITH MERIDIANS' (U+1F310)
- URL: BuildInfo.WebsiteUrl
+ URL: BuildInfo.RepositoryUrl
);
var wikiButton = new ButtonComponent(
@@ -131,7 +131,7 @@ public sealed class AboutCommandGroup : CommandGroup
return await _feedback.SendContextualEmbedResultAsync(embed,
new FeedbackMessageOptions(MessageComponents: new[]
{
- new ActionRowComponent([repositoryButton, wikiButton, issuesButton])
+ new ActionRowComponent(new[] { repositoryButton, wikiButton, issuesButton })
}), ct);
}
}
diff --git a/TeamOctolings.Octobot/Commands/BanCommandGroup.cs b/TeamOctolings.Octobot/Commands/BanCommandGroup.cs
index 1d6b26c..db3a8ce 100644
--- a/TeamOctolings.Octobot/Commands/BanCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/BanCommandGroup.cs
@@ -62,7 +62,7 @@ public sealed class BanCommandGroup : CommandGroup
///
///
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user
- /// was banned and vice versa.
+ /// was banned and vice-versa.
///
///
[Command("ban", "бан")]
@@ -128,7 +128,7 @@ public sealed class BanCommandGroup : CommandGroup
return await BanUserAsync(executor, target, reason, timeSpan, guild, data, channelId, bot, CancellationToken);
}
- private async Task BanUserAsync(
+ public async Task BanUserAsync(
IUser executor, IUser target, string reason, TimeSpan? duration, IGuild guild, GuildData data,
Snowflake channelId,
IUser bot, CancellationToken ct = default)
@@ -219,7 +219,7 @@ public sealed class BanCommandGroup : CommandGroup
///
///
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user
- /// was unbanned and vice versa.
+ /// was unbanned and vice-versa.
///
///
///
diff --git a/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs b/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs
index 7f29581..7c1b516 100644
--- a/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs
@@ -51,7 +51,7 @@ public sealed class ClearCommandGroup : CommandGroup
/// The user whose messages will be cleared.
///
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that any messages
- /// were cleared and vice versa.
+ /// were cleared and vice-versa.
///
[Command("clear", "очистить")]
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
diff --git a/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs
index ff7339f..7409d3b 100644
--- a/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs
+++ b/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs
@@ -81,7 +81,7 @@ public sealed class ErrorLoggingPostExecutionEvent : IPostExecutionEvent
return ResultExtensions.FromError(await _feedback.SendContextualEmbedResultAsync(embed,
new FeedbackMessageOptions(MessageComponents: new[]
{
- new ActionRowComponent([issuesButton])
+ new ActionRowComponent(new[] { issuesButton })
}), ct)
);
}
diff --git a/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs b/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs
index f07b210..d7798f3 100644
--- a/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs
@@ -288,7 +288,7 @@ public sealed class InfoCommandGroup : CommandGroup
return await ShowGuildInfoAsync(bot, guild, CancellationToken);
}
- private Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct = default)
+ private Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct)
{
var description = new StringBuilder().AppendLine($"## {guild.Name}");
diff --git a/TeamOctolings.Octobot/Commands/KickCommandGroup.cs b/TeamOctolings.Octobot/Commands/KickCommandGroup.cs
index 3011375..ccbe2a1 100644
--- a/TeamOctolings.Octobot/Commands/KickCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/KickCommandGroup.cs
@@ -57,7 +57,7 @@ public sealed class KickCommandGroup : CommandGroup
///
///
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member
- /// was kicked and vice versa.
+ /// was kicked and vice-versa.
///
[Command("kick", "кик")]
[DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
@@ -111,7 +111,7 @@ public sealed class KickCommandGroup : CommandGroup
return await KickUserAsync(executor, target, reason, guild, channelId, data, bot, CancellationToken);
}
- private async Task KickUserAsync(
+ public async Task KickUserAsync(
IUser executor, IUser target, string reason, IGuild guild, Snowflake channelId, GuildData data, IUser bot,
CancellationToken ct = default)
{
diff --git a/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs
index 5dce0b6..5b5dff0 100644
--- a/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs
@@ -59,7 +59,7 @@ public sealed class MuteCommandGroup : CommandGroup
///
///
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member
- /// was muted and vice versa.
+ /// was muted and vice-versa.
///
///
[Command("mute", "мут")]
@@ -123,7 +123,7 @@ public sealed class MuteCommandGroup : CommandGroup
CancellationToken);
}
- private async Task MuteUserAsync(
+ public async Task MuteUserAsync(
IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data,
Snowflake channelId, IUser bot, CancellationToken ct = default)
{
@@ -170,7 +170,7 @@ public sealed 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 = default)
+ IUser bot, DateTimeOffset until, CancellationToken ct)
{
var muteRole = GuildSettings.MuteRole.Get(data.Settings);
@@ -186,7 +186,7 @@ public sealed class MuteCommandGroup : CommandGroup
private async Task RoleMuteUserAsync(
IUser executor, IUser target, string reason, Snowflake guildId, GuildData data,
- DateTimeOffset until, Snowflake muteRole, CancellationToken ct = default)
+ DateTimeOffset until, Snowflake muteRole, CancellationToken ct)
{
var assignRoles = new List { muteRole };
var memberData = data.GetOrCreateMemberData(target.ID);
@@ -208,7 +208,7 @@ public sealed class MuteCommandGroup : CommandGroup
private async Task TimeoutUserAsync(
IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId,
- IUser bot, DateTimeOffset until, CancellationToken ct = default)
+ IUser bot, DateTimeOffset until, CancellationToken ct)
{
if (duration.TotalDays >= 28)
{
@@ -235,7 +235,7 @@ public sealed class MuteCommandGroup : CommandGroup
///
///
/// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member
- /// was unmuted and vice versa.
+ /// was unmuted and vice-versa.
///
///
///
diff --git a/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs
index 3188d27..bf59d67 100644
--- a/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs
@@ -78,7 +78,7 @@ public sealed 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 = default)
+ private Task ListRemindersAsync(MemberData data, Snowflake guildId, IUser executor, IUser bot, CancellationToken ct)
{
if (data.Reminders.Count == 0)
{
@@ -94,7 +94,7 @@ public sealed class RemindCommandGroup : CommandGroup
{
var reminder = data.Reminders[i];
builder.AppendBulletPointLine(string.Format(Messages.ReminderPosition, Markdown.InlineCode((i + 1).ToString())))
- .AppendSubBulletPointLine(string.Format(Messages.ReminderText, reminder.Text))
+ .AppendSubBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(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}"));
}
@@ -182,7 +182,7 @@ public sealed class RemindCommandGroup : CommandGroup
});
var builder = new StringBuilder()
- .AppendLine(MarkdownExtensions.Quote(text))
+ .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(text)))
.AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt)));
var embed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.ReminderCreated, executor.GetTag()), executor)
@@ -279,7 +279,7 @@ public sealed class RemindCommandGroup : CommandGroup
data.Reminders.RemoveAt(index);
var builder = new StringBuilder()
- .AppendLine(MarkdownExtensions.Quote(oldReminder.Text))
+ .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(oldReminder.Text)))
.AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt)));
var embed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.ReminderEdited, executor.GetTag()), executor)
@@ -309,7 +309,7 @@ public sealed class RemindCommandGroup : CommandGroup
data.Reminders.RemoveAt(index);
var builder = new StringBuilder()
- .AppendLine(MarkdownExtensions.Quote(value))
+ .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(value)))
.AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(oldReminder.At)));
var embed = new EmbedBuilder().WithSmallTitle(
string.Format(Messages.ReminderEdited, executor.GetTag()), executor)
@@ -353,7 +353,7 @@ public sealed class RemindCommandGroup : CommandGroup
}
private Task DeleteReminderAsync(MemberData data, int index, IUser bot,
- CancellationToken ct = default)
+ CancellationToken ct)
{
if (index >= data.Reminders.Count)
{
@@ -367,7 +367,7 @@ public sealed class RemindCommandGroup : CommandGroup
var reminder = data.Reminders[index];
var description = new StringBuilder()
- .AppendLine(MarkdownExtensions.Quote(reminder.Text))
+ .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text)))
.AppendBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At)));
data.Reminders.RemoveAt(index);
diff --git a/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs b/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs
index 15aa42b..a03f20d 100644
--- a/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs
@@ -38,6 +38,7 @@ public sealed class SettingsCommandGroup : CommandGroup
private static readonly IGuildOption[] AllOptions =
[
GuildSettings.Language,
+ GuildSettings.WarnPunishment,
GuildSettings.WelcomeMessage,
GuildSettings.LeaveMessage,
GuildSettings.ReceiveStartupMessages,
@@ -45,6 +46,7 @@ public sealed class SettingsCommandGroup : CommandGroup
GuildSettings.ReturnRolesOnRejoin,
GuildSettings.AutoStartEvents,
GuildSettings.RenameHoistedUsers,
+ GuildSettings.WarnsThreshold,
GuildSettings.PublicFeedbackChannel,
GuildSettings.PrivateFeedbackChannel,
GuildSettings.WelcomeMessagesChannel,
@@ -53,7 +55,8 @@ public sealed class SettingsCommandGroup : CommandGroup
GuildSettings.MuteRole,
GuildSettings.ModeratorRole,
GuildSettings.EventNotificationRole,
- GuildSettings.EventEarlyNotificationOffset
+ GuildSettings.EventEarlyNotificationOffset,
+ GuildSettings.WarnPunishmentDuration
];
private readonly ICommandContext _context;
@@ -202,27 +205,6 @@ public sealed class SettingsCommandGroup : CommandGroup
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)
{
diff --git a/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs
index 2936392..b4c3488 100644
--- a/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs
+++ b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs
@@ -90,7 +90,7 @@ public sealed class ToolsCommandGroup : CommandGroup
}
private Task SendRandomNumberAsync(long first, long? secondNullable,
- IUser executor, CancellationToken ct = default)
+ IUser executor, CancellationToken ct)
{
const long secondDefault = 0;
var second = secondNullable ?? secondDefault;
@@ -187,7 +187,7 @@ public sealed class ToolsCommandGroup : CommandGroup
return await SendTimestampAsync(offset, executor, CancellationToken);
}
- private Task SendTimestampAsync(TimeSpan? offset, IUser executor, CancellationToken ct = default)
+ private Task SendTimestampAsync(TimeSpan? offset, IUser executor, CancellationToken ct)
{
var timestamp = DateTimeOffset.UtcNow.Add(offset ?? TimeSpan.Zero).ToUnixTimeSeconds();
@@ -249,7 +249,7 @@ public sealed class ToolsCommandGroup : CommandGroup
return await AnswerEightBallAsync(bot, CancellationToken);
}
- private Task AnswerEightBallAsync(IUser bot, CancellationToken ct = default)
+ private Task AnswerEightBallAsync(IUser bot, CancellationToken ct)
{
var typeNumber = Random.Shared.Next(0, 4);
var embedColor = typeNumber switch
diff --git a/TeamOctolings.Octobot/Commands/WarnCommandGroup.cs b/TeamOctolings.Octobot/Commands/WarnCommandGroup.cs
new file mode 100644
index 0000000..4b8d712
--- /dev/null
+++ b/TeamOctolings.Octobot/Commands/WarnCommandGroup.cs
@@ -0,0 +1,560 @@
+using System.Collections;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Text;
+using System.Text.Json.Nodes;
+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.Conditions;
+using Remora.Discord.Commands.Contexts;
+using Remora.Discord.Commands.Feedback.Services;
+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;
+using static System.DateTimeOffset;
+
+namespace TeamOctolings.Octobot.Commands;
+
+[UsedImplicitly]
+public class WarnCommandGroup : CommandGroup
+{
+ private readonly AccessControlService _access;
+ private readonly IDiscordRestChannelAPI _channelApi;
+ private readonly ICommandContext _context;
+ private readonly IFeedbackService _feedback;
+ private readonly IDiscordRestGuildAPI _guildApi;
+ private readonly GuildDataService _guildData;
+ private readonly IDiscordRestUserAPI _userApi;
+ private readonly Utility _utility;
+
+ public WarnCommandGroup(
+ ICommandContext context, IDiscordRestChannelAPI channelApi, GuildDataService guildData,
+ IFeedbackService feedback, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi,
+ Utility utility, AccessControlService access)
+ {
+ _context = context;
+ _channelApi = channelApi;
+ _guildData = guildData;
+ _feedback = feedback;
+ _guildApi = guildApi;
+ _userApi = userApi;
+ _utility = utility;
+ _access = access;
+ }
+
+ [Command("warn")]
+ [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
+ [DiscordDefaultDMPermission(false)]
+ [RequireContext(ChannelContext.Guild)]
+ [RequireDiscordPermission(DiscordPermission.ManageMessages)]
+ [RequireBotDiscordPermissions(DiscordPermission.KickMembers,
+ DiscordPermission.ModerateMembers, DiscordPermission.BanMembers)]
+ [Description("Warn user")]
+ [UsedImplicitly]
+ public async Task ExecuteWarnAsync(
+ [Description("User to warn")] IUser target,
+ [Description("Warn reason")] [MaxLength(256)]
+ string reason)
+ {
+ if (!_context.TryGetContextIDs(out var guildId, out var channelId, 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 guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
+ if (!guildResult.IsDefined(out var guild))
+ {
+ return Result.FromError(guildResult);
+ }
+
+ var data = await _guildData.GetData(guild.ID, CancellationToken);
+ Messages.Culture = GuildSettings.Language.Get(data.Settings);
+
+ var interactionResult
+ = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Warn", CancellationToken);
+ if (!interactionResult.IsSuccess)
+ {
+ return ResultExtensions.FromError(interactionResult);
+ }
+
+ if (interactionResult.Entity is not null)
+ {
+ var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot)
+ .WithColour(ColorsList.Red).Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: CancellationToken);
+ }
+
+ return await WarnPreparationAsync(executor, target, reason, guild, data, channelId, bot, CancellationToken);
+ }
+
+ private async Task WarnPreparationAsync(IUser executor, IUser target, string reason, IGuild guild,
+ GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default)
+ {
+ var memberData = data.GetOrCreateMemberData(target.ID);
+ var warns = memberData.Warns;
+
+ var settings = data.Settings;
+
+ var warnThreshold = GuildSettings.WarnsThreshold.Get(settings);
+ var warnPunishment = GuildSettings.WarnPunishment.Get(settings);
+ var warnDuration = GuildSettings.WarnPunishmentDuration.Get(settings);
+
+ if (warnPunishment is "off" or "disable" or "disabled"
+ && warns.Count + 1 >= warnThreshold
+ && warnThreshold is not 0)
+ {
+ var errorEmbed = new EmbedBuilder()
+ .WithSmallTitle(string.Format(Messages.WarnThresholdExceeded, warnThreshold), bot)
+ .WithDescription(Messages.WarnThresholdExceededDescription)
+ .WithColour(ColorsList.Red).Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: CancellationToken);
+ }
+
+ if (warnPunishment is "ban" or "mute" && warnDuration == TimeSpan.Zero)
+ {
+ var errorEmbed = new EmbedBuilder()
+ .WithSmallTitle(Messages.WarnPunishmentDurationNotSet, bot)
+ .WithColour(ColorsList.Red).Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: CancellationToken);
+ }
+
+ if (warns.Count + 1 < warnThreshold || warnThreshold is 0)
+ {
+ return await WarnUserAsync(executor, target, reason, guild, data, channelId, bot, settings,
+ warns, warnThreshold, warnPunishment, warnDuration, ct);
+ }
+
+ var interactionResult
+ = await _access.CheckInteractionsAsync(guild.ID, bot.ID, target.ID,
+ $"{char.ToUpperInvariant(warnPunishment[0])}{warnPunishment[1..]}", ct);
+ if (!interactionResult.IsSuccess)
+ {
+ return ResultExtensions.FromError(interactionResult);
+ }
+
+ if (interactionResult.Entity is not null)
+ {
+ var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot)
+ .WithColour(ColorsList.Red).Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct);
+ }
+
+ return await WarnUserAsync(executor, target, reason, guild, data, channelId, bot, settings,
+ warns, warnThreshold, warnPunishment, warnDuration, ct);
+ }
+
+ private async Task WarnUserAsync(IUser executor, IUser target, string reason, IGuild guild,
+ GuildData data, Snowflake channelId, IUser bot, JsonNode settings, IList warns, int warnThreshold,
+ string warnPunishment, TimeSpan warnDuration, CancellationToken ct = default)
+ {
+ warns.Add(new Warn
+ {
+ WarnedBy = executor.ID.Value,
+ At = UtcNow,
+ Reason = reason
+ });
+
+ var builder = new StringBuilder()
+ .AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason))
+ .AppendBulletPointLine(string.Format(Messages.DescriptionWarns,
+ warnThreshold is 0 ? warns.Count : $"{warns.Count}/{warnThreshold}"));
+
+ var title = string.Format(Messages.UserWarned, target.GetTag());
+ var description = builder.ToString();
+
+ var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct);
+ if (dmChannelResult.IsDefined(out var dmChannel))
+ {
+ var dmEmbed = new EmbedBuilder().WithGuildTitle(guild)
+ .WithTitle(Messages.YouHaveBeenWarned)
+ .WithDescription(description)
+ .WithActionFooter(executor)
+ .WithCurrentTimestamp()
+ .WithColour(ColorsList.Yellow)
+ .Build();
+
+ await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct);
+ }
+
+ _utility.LogAction(settings, channelId, executor, title, description,
+ target, ColorsList.Yellow, false, ct);
+
+ var embed = new EmbedBuilder().WithSmallTitle(title, target)
+ .WithColour(ColorsList.Green).Build();
+
+ if (warns.Count >= warnThreshold && warnThreshold is not 0)
+ {
+ return await PunishUserAsync(target, guild, data, channelId, bot, warns, warnPunishment, warnDuration, CancellationToken);
+ }
+
+ return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
+ }
+
+ private async Task PunishUserAsync(IUser target, IGuild guild, GuildData data,
+ Snowflake channelId, IUser bot, IList warns, string punishment, TimeSpan duration, CancellationToken ct)
+ {
+ if (punishment is "ban" && duration != TimeSpan.Zero)
+ {
+ var banCommandGroup = new BanCommandGroup(
+ _access, _channelApi, _context, _feedback, _guildApi, _guildData, _userApi, _utility);
+ await banCommandGroup.BanUserAsync(bot, target, Messages.ReceivedTooManyWarnings,
+ duration, guild, data, channelId, bot, ct);
+ }
+
+ if (punishment is "kick")
+ {
+ var kickCommandGroup = new KickCommandGroup(
+ _access, _channelApi, _context, _feedback, _guildApi, _guildData, _userApi, _utility);
+ await kickCommandGroup.KickUserAsync(bot, target, Messages.ReceivedTooManyWarnings,
+ guild, channelId, data, bot, ct);
+ }
+
+ if (punishment is "mute" && duration != TimeSpan.Zero)
+ {
+ var muteCommandGroup = new MuteCommandGroup(
+ _access, _context, _feedback, _guildApi, _guildData, _userApi, _utility);
+ await muteCommandGroup.MuteUserAsync(bot, target, Messages.ReceivedTooManyWarnings,
+ duration, guild.ID, data, channelId, bot, ct);
+ }
+
+ warns.Clear();
+ return Result.FromSuccess();
+ }
+
+ [Command("unwarn")]
+ [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)]
+ [DiscordDefaultDMPermission(false)]
+ [RequireContext(ChannelContext.Guild)]
+ [RequireDiscordPermission(DiscordPermission.ManageMessages)]
+ [Description("Remove warns from user")]
+ [UsedImplicitly]
+ public async Task ExecuteUnwarnAsync(
+ [Description("User to remove warns from")]
+ IUser target,
+ [Description("Warn remove reason")] [MaxLength(256)]
+ string reason,
+ [Description("Number of the warning to be deleted")]
+ int? number = null)
+ {
+ if (!_context.TryGetContextIDs(out var guildId, out var channelId, 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 guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
+ if (!guildResult.IsDefined(out var guild))
+ {
+ return Result.FromError(guildResult);
+ }
+
+ var data = await _guildData.GetData(guild.ID, CancellationToken);
+ Messages.Culture = GuildSettings.Language.Get(data.Settings);
+
+ var interactionResult
+ = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "Unwarn", CancellationToken);
+ if (!interactionResult.IsSuccess)
+ {
+ return ResultExtensions.FromError(interactionResult);
+ }
+
+ if (interactionResult.Entity is not null)
+ {
+ var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot)
+ .WithColour(ColorsList.Red).Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: CancellationToken);
+ }
+
+ if (number is not null)
+ {
+ return await RemoveUserWarnAsync(executor, target, reason, number.Value, guild, data, channelId, bot,
+ CancellationToken);
+ }
+
+ return await RemoveUserWarnsAsync(executor, target, reason, guild, data, channelId, bot, CancellationToken);
+ }
+
+ private async Task RemoveUserWarnAsync(IUser executor, IUser target, string reason, int warnNumber,
+ IGuild guild, GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default)
+ {
+ var memberData = data.GetOrCreateMemberData(target.ID);
+ var warns = memberData.Warns;
+
+ if (warns.Count is 0)
+ {
+ var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserHasNoWarnings, bot)
+ .WithColour(ColorsList.Red).Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
+ }
+
+ var index = warnNumber - 1;
+
+ if (index >= warns.Count || index < 0)
+ {
+ var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.WrongWarningNumberSelected, bot)
+ .WithColour(ColorsList.Red).Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
+ }
+
+ var builder = new StringBuilder()
+ .Append("> ").AppendLine(warns[index].Reason)
+ .AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason));
+
+ warns.RemoveAt(index);
+
+ var title = string.Format(Messages.UserWarnRemoved, warnNumber, target.GetTag());
+ var description = builder.ToString();
+
+ var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct);
+ if (dmChannelResult.IsDefined(out var dmChannel))
+ {
+ var dmEmbed = new EmbedBuilder().WithGuildTitle(guild)
+ .WithTitle(Messages.YourWarningHasBeenRevoked)
+ .WithDescription(description)
+ .WithActionFooter(executor)
+ .WithCurrentTimestamp()
+ .WithColour(ColorsList.Green)
+ .Build();
+
+ await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct);
+ }
+
+ var embed = new EmbedBuilder().WithSmallTitle(
+ title, target)
+ .WithColour(ColorsList.Green).Build();
+
+ _utility.LogAction(
+ data.Settings, channelId, executor, title, description, target, ColorsList.Yellow, false, ct);
+
+ return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
+ }
+
+ private async Task RemoveUserWarnsAsync(IUser executor, IUser target, string reason,
+ IGuild guild, GuildData data, Snowflake channelId, IUser bot, CancellationToken ct = default)
+ {
+ var memberData = data.GetOrCreateMemberData(target.ID);
+ var warns = memberData.Warns;
+
+ if (warns.Count is 0)
+ {
+ var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserHasNoWarnings, bot)
+ .WithColour(ColorsList.Red).Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
+ }
+
+ var builder = new StringBuilder().AppendBulletPointLine(string.Format(Messages.DescriptionActionReason, reason));
+
+ warns.Clear();
+
+ var title = string.Format(Messages.UserWarnsRemoved, target.GetTag());
+ var description = builder.ToString();
+
+ var dmChannelResult = await _userApi.CreateDMAsync(target.ID, ct);
+ if (dmChannelResult.IsDefined(out var dmChannel))
+ {
+ var dmEmbed = new EmbedBuilder().WithGuildTitle(guild)
+ .WithTitle(Messages.YourWarningsHaveBeenRevoked)
+ .WithDescription(description)
+ .WithActionFooter(executor)
+ .WithCurrentTimestamp()
+ .WithColour(ColorsList.Green)
+ .Build();
+
+ await _channelApi.CreateMessageWithEmbedResultAsync(dmChannel.ID, embedResult: dmEmbed, ct: ct);
+ }
+
+ var embed = new EmbedBuilder().WithSmallTitle(
+ title, target)
+ .WithColour(ColorsList.Green).Build();
+
+ _utility.LogAction(
+ data.Settings, channelId, executor, title, description, target, ColorsList.Yellow, false, ct);
+
+ return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
+ }
+
+ [Command("listwarn")]
+ [DiscordDefaultDMPermission(false)]
+ [Ephemeral]
+ [Description("(Ephemeral) Get current warns")]
+ [UsedImplicitly]
+ public async Task ExecuteListWarnsAsync(
+ [Description("(Moderator-only) Get target's current warns")]
+ IUser? target = 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 guildResult = await _guildApi.GetGuildAsync(guildId, ct: CancellationToken);
+ if (!guildResult.IsDefined(out var guild))
+ {
+ return Result.FromError(guildResult);
+ }
+
+ var data = await _guildData.GetData(guild.ID, CancellationToken);
+ Messages.Culture = GuildSettings.Language.Get(data.Settings);
+
+ if (target is not null)
+ {
+ return await ListTargetWarnsAsync(executor, target, guild, data, bot, CancellationToken);
+ }
+
+ return await ListExecutorWarnsAsync(executor, data, bot, CancellationToken);
+ }
+
+ private async Task ListTargetWarnsAsync(IUser executor, IUser target, IGuild guild,
+ GuildData data, IUser bot, CancellationToken ct = default)
+ {
+ var interactionResult
+ = await _access.CheckInteractionsAsync(guild.ID, executor.ID, target.ID, "GetWarns", ct);
+ if (!interactionResult.IsSuccess)
+ {
+ return ResultExtensions.FromError(interactionResult);
+ }
+
+ if (interactionResult.Entity is not null)
+ {
+ var errorEmbed = new EmbedBuilder().WithSmallTitle(interactionResult.Entity, bot)
+ .WithColour(ColorsList.Red).Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(errorEmbed, ct: ct);
+ }
+
+ var memberData = data.GetOrCreateMemberData(target.ID);
+ var warns = memberData.Warns;
+
+ if (warns.Count is 0)
+ {
+ var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.UserHasNoWarnings, bot)
+ .WithColour(ColorsList.Green).Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
+ }
+
+ var warnThreshold = GuildSettings.WarnsThreshold.Get(data.Settings);
+
+ var punishmentType = GuildSettings.WarnPunishment.Get(data.Settings);
+
+ var description = new StringBuilder()
+ .AppendLine(string.Format(Messages.DescriptionWarns,
+ warnThreshold is 0 ? warns.Count : $"{warns.Count}/{warnThreshold}"));
+ if (punishmentType is not "off" and not "disable" and not "disabled")
+ {
+ description.AppendLine(string.Format(
+ Messages.DescriptionPunishmentType, Markdown.InlineCode(punishmentType)));
+ }
+
+ var warnCount = 0;
+ foreach (var warn in warns)
+ {
+ warnCount++;
+ description.Append(warnCount).Append(". ").AppendLine(warn.Reason)
+ .AppendSubBulletPoint(Messages.IssuedBy).Append(' ').AppendLine(Mention.User(warn.WarnedBy.ToSnowflake()))
+ .AppendSubBulletPointLine(string.Format(Messages.ReceivedOn, Markdown.Timestamp(warn.At)));
+ }
+
+ var embed = new EmbedBuilder()
+ .WithSmallTitle(string.Format(Messages.ListTargetWarnsTitle, target.GetTag()), target)
+ .WithDescription(description.ToString())
+ .WithColour(ColorsList.Default).Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
+ }
+
+ private async Task ListExecutorWarnsAsync(IUser executor, GuildData data, IUser bot,
+ CancellationToken ct = default)
+ {
+ var memberData = data.GetOrCreateMemberData(executor.ID);
+ var warns = memberData.Warns;
+
+ if (warns.Count is 0)
+ {
+ var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.YouHaveNoWarnings, bot)
+ .WithColour(ColorsList.Green).Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
+ }
+
+ var warnThreshold = GuildSettings.WarnsThreshold.Get(data.Settings);
+
+ var punishmentType = GuildSettings.WarnPunishment.Get(data.Settings);
+
+ var description = new StringBuilder()
+ .AppendLine(string.Format(Messages.DescriptionWarns,
+ warnThreshold is 0 ? warns.Count : $"{warns.Count}/{warnThreshold}"));
+ if (punishmentType is not "off" and not "disable" and not "disabled")
+ {
+ description.AppendLine(string.Format(
+ Messages.DescriptionPunishmentType, Markdown.InlineCode(punishmentType)));
+ }
+
+ var warnCount = 0;
+ foreach (var warn in warns)
+ {
+ warnCount++;
+ description.Append(warnCount).Append(". ").AppendLine(warn.Reason)
+ .AppendSubBulletPoint(Messages.IssuedBy).Append(' ').AppendLine(Mention.User(warn.WarnedBy.ToSnowflake()))
+ .AppendSubBulletPointLine(string.Format(Messages.ReceivedOn, Markdown.Timestamp(warn.At)));
+ }
+
+ var embed = new EmbedBuilder()
+ .WithSmallTitle(string.Format(Messages.ListExecutorWarnsTitle, executor.GetTag()), executor)
+ .WithDescription(description.ToString())
+ .WithColour(ColorsList.Default).Build();
+
+ return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
+ }
+}
diff --git a/TeamOctolings.Octobot/Data/GuildSettings.cs b/TeamOctolings.Octobot/Data/GuildSettings.cs
index dc59d6f..26e325a 100644
--- a/TeamOctolings.Octobot/Data/GuildSettings.cs
+++ b/TeamOctolings.Octobot/Data/GuildSettings.cs
@@ -12,6 +12,8 @@ public static class GuildSettings
{
public static readonly LanguageOption Language = new("Language", "en");
+ public static readonly PunishmentOption WarnPunishment = new("WarnPunishment", "disabled");
+
///
/// Controls what message should be sent in when a new member joins the guild.
///
@@ -58,6 +60,8 @@ public static class GuildSettings
///
public static readonly BoolOption RenameHoistedUsers = new("RenameHoistedUsers", false);
+ public static readonly IntOption WarnsThreshold = new("WarnsThreshold", 0);
+
///
/// Controls what channel should all public messages be sent to.
///
@@ -84,4 +88,7 @@ public static class GuildSettings
///
public static readonly TimeSpanOption EventEarlyNotificationOffset = new(
"EventEarlyNotificationOffset", TimeSpan.Zero);
+
+ public static readonly TimeSpanOption WarnPunishmentDuration = new(
+ "WarnPunishmentDuration", TimeSpan.Zero);
}
diff --git a/TeamOctolings.Octobot/Data/MemberData.cs b/TeamOctolings.Octobot/Data/MemberData.cs
index 984d4af..57b5cc9 100644
--- a/TeamOctolings.Octobot/Data/MemberData.cs
+++ b/TeamOctolings.Octobot/Data/MemberData.cs
@@ -5,13 +5,18 @@ namespace TeamOctolings.Octobot.Data;
///
public sealed class MemberData
{
- public MemberData(ulong id, List? reminders = null)
+ public MemberData(ulong id, List? reminders = null, List? warns = null)
{
Id = id;
if (reminders is not null)
{
Reminders = reminders;
}
+
+ if (warns is not null)
+ {
+ Warns = warns;
+ }
}
public ulong Id { get; }
@@ -20,4 +25,5 @@ public sealed class MemberData
public bool Kicked { get; set; }
public List Roles { get; set; } = [];
public List Reminders { get; } = [];
+ public List Warns { get; } = [];
}
diff --git a/TeamOctolings.Octobot/Data/Options/AllOptionsEnum.cs b/TeamOctolings.Octobot/Data/Options/AllOptionsEnum.cs
index 6a4280e..53b7f63 100644
--- a/TeamOctolings.Octobot/Data/Options/AllOptionsEnum.cs
+++ b/TeamOctolings.Octobot/Data/Options/AllOptionsEnum.cs
@@ -13,6 +13,7 @@ namespace TeamOctolings.Octobot.Data.Options;
public enum AllOptionsEnum
{
[UsedImplicitly] Language,
+ [UsedImplicitly] WarnPunishment,
[UsedImplicitly] WelcomeMessage,
[UsedImplicitly] LeaveMessage,
[UsedImplicitly] ReceiveStartupMessages,
@@ -20,6 +21,7 @@ public enum AllOptionsEnum
[UsedImplicitly] ReturnRolesOnRejoin,
[UsedImplicitly] AutoStartEvents,
[UsedImplicitly] RenameHoistedUsers,
+ [UsedImplicitly] WarnsThreshold,
[UsedImplicitly] PublicFeedbackChannel,
[UsedImplicitly] PrivateFeedbackChannel,
[UsedImplicitly] WelcomeMessagesChannel,
@@ -28,5 +30,6 @@ public enum AllOptionsEnum
[UsedImplicitly] MuteRole,
[UsedImplicitly] ModeratorRole,
[UsedImplicitly] EventNotificationRole,
- [UsedImplicitly] EventEarlyNotificationOffset
+ [UsedImplicitly] EventEarlyNotificationOffset,
+ [UsedImplicitly] WarnPunishmentDuration
}
diff --git a/TeamOctolings.Octobot/Data/Options/BoolOption.cs b/TeamOctolings.Octobot/Data/Options/BoolOption.cs
index 3b81abb..6a3c899 100644
--- a/TeamOctolings.Octobot/Data/Options/BoolOption.cs
+++ b/TeamOctolings.Octobot/Data/Options/BoolOption.cs
@@ -12,16 +12,6 @@ public sealed class BoolOption : GuildOption
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))
diff --git a/TeamOctolings.Octobot/Data/Options/GuildOption.cs b/TeamOctolings.Octobot/Data/Options/GuildOption.cs
index ea9c30e..5d9f1a2 100644
--- a/TeamOctolings.Octobot/Data/Options/GuildOption.cs
+++ b/TeamOctolings.Octobot/Data/Options/GuildOption.cs
@@ -21,19 +21,9 @@ public class GuildOption : IGuildOption
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(Value(settings));
- }
-
- public virtual Result ValueEquals(JsonNode settings, string value)
- {
- return Value(settings).Equals(value);
+ return Markdown.InlineCode(Get(settings).ToString() ?? throw new InvalidOperationException());
}
///
diff --git a/TeamOctolings.Octobot/Data/Options/IGuildOption.cs b/TeamOctolings.Octobot/Data/Options/IGuildOption.cs
index 9920281..a8c3e6e 100644
--- a/TeamOctolings.Octobot/Data/Options/IGuildOption.cs
+++ b/TeamOctolings.Octobot/Data/Options/IGuildOption.cs
@@ -7,7 +7,6 @@ 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/TeamOctolings.Octobot/Data/Options/IntOption.cs b/TeamOctolings.Octobot/Data/Options/IntOption.cs
new file mode 100644
index 0000000..3cdeb39
--- /dev/null
+++ b/TeamOctolings.Octobot/Data/Options/IntOption.cs
@@ -0,0 +1,31 @@
+using System.Text.Json.Nodes;
+using Remora.Results;
+
+namespace TeamOctolings.Octobot.Data.Options;
+
+public sealed class IntOption : GuildOption
+{
+ public IntOption(string name, int defaultValue) : base(name, defaultValue) { }
+
+ public override string Display(JsonNode settings)
+ {
+ return settings[Name]?.GetValue() ?? "0";
+ }
+
+ public override Result Set(JsonNode settings, string from)
+ {
+ if (!int.TryParse(from, out _))
+ {
+ return new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue);
+ }
+
+ settings[Name] = from;
+ return Result.FromSuccess();
+ }
+
+ public override int Get(JsonNode settings)
+ {
+ var property = settings[Name];
+ return property != null ? Convert.ToInt32(property.GetValue()) : DefaultValue;
+ }
+}
diff --git a/TeamOctolings.Octobot/Data/Options/LanguageOption.cs b/TeamOctolings.Octobot/Data/Options/LanguageOption.cs
index f58e011..15ab6ff 100644
--- a/TeamOctolings.Octobot/Data/Options/LanguageOption.cs
+++ b/TeamOctolings.Octobot/Data/Options/LanguageOption.cs
@@ -1,5 +1,6 @@
using System.Globalization;
using System.Text.Json.Nodes;
+using Remora.Discord.Extensions.Formatting;
using Remora.Results;
namespace TeamOctolings.Octobot.Data.Options;
@@ -15,9 +16,9 @@ public sealed class LanguageOption : GuildOption
public LanguageOption(string name, string defaultValue) : base(name, CultureInfoCache[defaultValue]) { }
- protected override string Value(JsonNode settings)
+ public override string Display(JsonNode settings)
{
- return settings[Name]?.GetValue() ?? "en";
+ return Markdown.InlineCode(settings[Name]?.GetValue() ?? "en");
}
///
diff --git a/TeamOctolings.Octobot/Data/Options/PunishmentOption.cs b/TeamOctolings.Octobot/Data/Options/PunishmentOption.cs
new file mode 100644
index 0000000..b65ee68
--- /dev/null
+++ b/TeamOctolings.Octobot/Data/Options/PunishmentOption.cs
@@ -0,0 +1,23 @@
+using System.Text.Json.Nodes;
+using Remora.Results;
+
+namespace TeamOctolings.Octobot.Data.Options;
+
+///
+public sealed class PunishmentOption : GuildOption
+{
+ private static readonly List AllowedValues =
+ [
+ "ban", "kick", "mute", "off", "disable", "disabled"
+ ];
+
+ public PunishmentOption(string name, string defaultValue) : base(name, defaultValue) { }
+
+ ///
+ public override Result Set(JsonNode settings, string from)
+ {
+ return AllowedValues.Contains(from.ToLowerInvariant())
+ ? base.Set(settings, from.ToLowerInvariant())
+ : new ArgumentInvalidError(nameof(from), Messages.InvalidWarnPunishment);
+ }
+}
diff --git a/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs b/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs
index 7e21343..3501f09 100644
--- a/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs
+++ b/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs
@@ -8,16 +8,6 @@ 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];
diff --git a/TeamOctolings.Octobot/Data/Reminder.cs b/TeamOctolings.Octobot/Data/Reminder.cs
index 40f29e1..c3936da 100644
--- a/TeamOctolings.Octobot/Data/Reminder.cs
+++ b/TeamOctolings.Octobot/Data/Reminder.cs
@@ -1,9 +1,9 @@
namespace TeamOctolings.Octobot.Data;
-public sealed record Reminder
+public struct Reminder
{
- public required DateTimeOffset At { get; init; }
- public required string Text { get; init; }
- public required ulong ChannelId { get; init; }
- public required ulong MessageId { get; init; }
+ public DateTimeOffset At { get; init; }
+ public string Text { get; init; }
+ public ulong ChannelId { get; init; }
+ public ulong MessageId { get; init; }
}
diff --git a/TeamOctolings.Octobot/Data/Warn.cs b/TeamOctolings.Octobot/Data/Warn.cs
new file mode 100644
index 0000000..ab512e6
--- /dev/null
+++ b/TeamOctolings.Octobot/Data/Warn.cs
@@ -0,0 +1,8 @@
+namespace TeamOctolings.Octobot.Data;
+
+public struct Warn
+{
+ public ulong WarnedBy { get; init; }
+ public DateTimeOffset At { get; init; }
+ public string Reason { get; init; }
+}
diff --git a/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs b/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs
index 7822d9b..b8eb2d1 100644
--- a/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs
@@ -10,7 +10,7 @@ public static class GuildScheduledEventExtensions
out string? location)
{
endTime = default;
- location = null;
+ location = default;
if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata))
{
return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata));
diff --git a/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs b/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs
index fac4dda..76fa386 100644
--- a/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs
@@ -25,7 +25,7 @@ public static class LoggerExtensions
if (result.Error is ExceptionError exe)
{
- if (exe.Exception is OperationCanceledException)
+ if (exe.Exception is TaskCanceledException)
{
return;
}
diff --git a/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs b/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs
index 30ddff5..202cd37 100644
--- a/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs
@@ -13,16 +13,4 @@ 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
index 6872d34..d7ef7d7 100644
--- a/TeamOctolings.Octobot/Extensions/ResultExtensions.cs
+++ b/TeamOctolings.Octobot/Extensions/ResultExtensions.cs
@@ -23,7 +23,7 @@ public static class ResultExtensions
private static void LogResultStackTrace(Result result)
{
- if (result.IsSuccess || result.Error is ExceptionError { Exception: OperationCanceledException })
+ if (result.IsSuccess)
{
return;
}
diff --git a/TeamOctolings.Octobot/Messages.Designer.cs b/TeamOctolings.Octobot/Messages.Designer.cs
index 1a81e02..d56dc77 100644
--- a/TeamOctolings.Octobot/Messages.Designer.cs
+++ b/TeamOctolings.Octobot/Messages.Designer.cs
@@ -9,21 +9,21 @@
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 {
@@ -34,7 +34,7 @@ namespace TeamOctolings.Octobot {
return resourceMan;
}
}
-
+
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Globalization.CultureInfo Culture {
get {
@@ -44,1162 +44,1269 @@ namespace TeamOctolings.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 Generic1 {
get {
return ResourceManager.GetString("Generic1", resourceCulture);
}
}
-
+
internal static string Generic2 {
get {
return ResourceManager.GetString("Generic2", resourceCulture);
}
}
-
+
internal static string Generic3 {
get {
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 SettingsLanguage {
get {
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 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 UserCannotMuteMembers {
get {
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 ButtonOpenWebsite {
+
+ internal static string ButtonOpenRepository {
get {
- return ResourceManager.GetString("ButtonOpenWebsite", resourceCulture);
+ 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 UserInfoDisplayName {
get {
return ResourceManager.GetString("UserInfoDisplayName", resourceCulture);
}
}
-
+
internal static string InformationAbout {
get {
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 {
get {
return ResourceManager.GetString("RandomTitle", resourceCulture);
}
}
-
+
internal static string RandomMinMaxSame {
get {
return ResourceManager.GetString("RandomMinMaxSame", resourceCulture);
}
}
-
+
internal static string RandomMin {
get {
return ResourceManager.GetString("RandomMin", resourceCulture);
}
}
-
+
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 {
return ResourceManager.GetString("TimestampTitle", resourceCulture);
}
}
-
+
internal static string TimestampOffset {
get {
return ResourceManager.GetString("TimestampOffset", resourceCulture);
}
}
-
+
internal static string GuildInfoDescription {
get {
return ResourceManager.GetString("GuildInfoDescription", resourceCulture);
}
}
-
+
internal static string GuildInfoCreatedAt {
get {
return ResourceManager.GetString("GuildInfoCreatedAt", resourceCulture);
}
}
-
+
internal static string GuildInfoOwner {
get {
return ResourceManager.GetString("GuildInfoOwner", resourceCulture);
}
}
-
+
internal static string GuildInfoServerBoost {
get {
return ResourceManager.GetString("GuildInfoServerBoost", resourceCulture);
}
}
-
+
internal static string GuildInfoBoostTier {
get {
return ResourceManager.GetString("GuildInfoBoostTier", resourceCulture);
}
}
-
+
internal static string GuildInfoBoostCount {
get {
return ResourceManager.GetString("GuildInfoBoostCount", resourceCulture);
}
}
-
+
internal static string NoMessagesToClear {
get {
return ResourceManager.GetString("NoMessagesToClear", resourceCulture);
}
}
-
+
internal static string MessagesClearedFiltered {
get {
return ResourceManager.GetString("MessagesClearedFiltered", resourceCulture);
}
}
-
+
internal static string DataLoadFailedTitle {
get {
return ResourceManager.GetString("DataLoadFailedTitle", resourceCulture);
}
}
-
+
internal static string DataLoadFailedDescription {
get {
return ResourceManager.GetString("DataLoadFailedDescription", resourceCulture);
}
}
-
+
internal static string CommandExecutionFailed {
get {
return ResourceManager.GetString("CommandExecutionFailed", resourceCulture);
}
}
-
+
internal static string ContactDevelopers {
get {
return ResourceManager.GetString("ContactDevelopers", resourceCulture);
}
}
-
+
internal static string ButtonReportIssue {
get {
return ResourceManager.GetString("ButtonReportIssue", resourceCulture);
}
}
-
+
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 {
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 UserWarned {
+ get {
+ return ResourceManager.GetString("UserWarned", resourceCulture);
+ }
+ }
+
+ internal static string UserWarnsRemoved {
+ get {
+ return ResourceManager.GetString("UserWarnsRemoved", resourceCulture);
+ }
+ }
+
+ internal static string YouHaveBeenWarned {
+ get {
+ return ResourceManager.GetString("YouHaveBeenWarned", resourceCulture);
+ }
+ }
+
+ internal static string YourWarningsHaveBeenRevoked {
+ get {
+ return ResourceManager.GetString("YourWarningsHaveBeenRevoked", resourceCulture);
+ }
+ }
+
+ internal static string DescriptionWarns {
+ get {
+ return ResourceManager.GetString("DescriptionWarns", resourceCulture);
+ }
+ }
+
+ internal static string UserHasNoWarnings {
+ get {
+ return ResourceManager.GetString("UserHasNoWarnings", resourceCulture);
+ }
+ }
+
+ internal static string YouHaveNoWarnings {
+ get {
+ return ResourceManager.GetString("YouHaveNoWarnings", resourceCulture);
+ }
+ }
+
+ internal static string ReceivedTooManyWarnings {
+ get {
+ return ResourceManager.GetString("ReceivedTooManyWarnings", 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 {
+
+ internal static string ListTargetWarnsTitle {
get {
- return ResourceManager.GetString("SettingValueEquals", resourceCulture);
+ return ResourceManager.GetString("ListTargetWarnsTitle", resourceCulture);
+ }
+ }
+
+ internal static string ReceivedOn {
+ get {
+ return ResourceManager.GetString("ReceivedOn", resourceCulture);
+ }
+ }
+
+ internal static string UserWarnRemoved {
+ get {
+ return ResourceManager.GetString("UserWarnRemoved", resourceCulture);
+ }
+ }
+
+ internal static string YourWarningHasBeenRevoked {
+ get {
+ return ResourceManager.GetString("YourWarningHasBeenRevoked", resourceCulture);
+ }
+ }
+
+ internal static string WrongWarningNumberSelected {
+ get {
+ return ResourceManager.GetString("WrongWarningNumberSelected", resourceCulture);
+ }
+ }
+
+ internal static string ListExecutorWarnsTitle {
+ get {
+ return ResourceManager.GetString("ListExecutorWarnsTitle", resourceCulture);
+ }
+ }
+
+ internal static string DescriptionPunishmentType {
+ get {
+ return ResourceManager.GetString("DescriptionPunishmentType", resourceCulture);
+ }
+ }
+
+ internal static string WarnThresholdExceeded {
+ get {
+ return ResourceManager.GetString("WarnThresholdExceeded", resourceCulture);
+ }
+ }
+
+ internal static string WarnPunishmentDurationNotSet {
+ get {
+ return ResourceManager.GetString("WarnPunishmentDurationNotSet", resourceCulture);
+ }
+ }
+
+ internal static string WarnThresholdExceededDescription {
+ get {
+ return ResourceManager.GetString("WarnThresholdExceededDescription", resourceCulture);
+ }
+ }
+
+ internal static string InvalidWarnPunishment {
+ get {
+ return ResourceManager.GetString("InvalidWarnPunishment", resourceCulture);
}
}
}
diff --git a/TeamOctolings.Octobot/Messages.resx b/TeamOctolings.Octobot/Messages.resx
index e4107fb..037ad3f 100644
--- a/TeamOctolings.Octobot/Messages.resx
+++ b/TeamOctolings.Octobot/Messages.resx
@@ -399,8 +399,8 @@
Developers:
-
- Open Website
+
+ Octobot's source code
About {0}
@@ -681,7 +681,124 @@
Moderator role
-
- The setting value is the same as the input value.
+
+ {0} received a warning
+
+
+ {0} no longer has warnings
+
+
+ You have been warned
+
+
+ Your warnings have been revoked
+
+
+ Warns: {0}
+
+
+ This user has no warnings!
+
+
+ Received too many warnings
+
+
+ Punishment type for warnings
+
+
+ Warnings threshold
+
+
+ Punishment duration for warnings
+
+
+ Here's your warnings, {0}:
+
+
+ You have no warnings!
+
+
+ Received on {0}
+
+
+ Warning #{0} has been removed from {1}
+
+
+ Your warning has been revoked
+
+
+ Wrong warning number selected!
+
+
+ You cannot warn me!
+
+
+ You cannot warn the owner of this guild!
+
+
+ You cannot warn this member!
+
+
+ You cannot warn yourself!
+
+
+ You cannot unwarn me!
+
+
+ You cannot unwarn the owner of this guild!
+
+
+ You cannot unwarn this member!
+
+
+ You cannot unwarn yourself!
+
+
+ I cannot warn members from this guild!
+
+
+ I cannot warn this member!
+
+
+ I cannot unwarn this member!
+
+
+ You cannot get my warns!
+
+
+ You cannot get owner's warns!
+
+
+ You cannot get warns of this member!
+
+
+ Use this command without options instead.
+
+
+ You cannot warn members in this guild!
+
+
+ You cannot unwarn members in this guild!
+
+
+ You cannot get warns of other members in this guild!
+
+
+ Warnings given to {0}:
+
+
+ Punishment type: {0}
+
+
+ Warn threshold has been exceeded. ({0})
+
+
+ Warn Punishment Duration is not set for the current punishment type.
+
+
+ Increase the Warn Threshold or set a Warn Punishment.
+
+
+ Invalid Warn Punishment is set.
diff --git a/TeamOctolings.Octobot/Messages.ru.resx b/TeamOctolings.Octobot/Messages.ru.resx
index d942cec..68062d4 100644
--- a/TeamOctolings.Octobot/Messages.ru.resx
+++ b/TeamOctolings.Octobot/Messages.ru.resx
@@ -399,8 +399,8 @@
Разработчики:
-
- Открыть веб-сайт
+
+ Исходный код Octobot
О боте {0}
@@ -681,7 +681,124 @@
Роль модератора
-
- Значение настройки такое же, как и вводное значение.
+
+ {0} получил предупреждение
+
+
+ {0} больше не имеет предупреждений
+
+
+ Вы получили предупреждение
+
+
+ Ваши предупреждения были отозваны
+
+
+ Предупреждений: {0}
+
+
+ Этот пользователь не имеет предупреждений!
+
+
+ Получил слишком много предупреждений
+
+
+ Тип наказания для предупреждений
+
+
+ Порог предупреждений
+
+
+ Длительность наказания для предупреждений
+
+
+ Вот ваши предупреждения, {0}:
+
+
+ У вас нет предупреждений!
+
+
+ Получено {0}
+
+
+ Предупреждение №{0} было снято с {1}
+
+
+ Ваше предупреждение было отозвано
+
+
+ Выбрано неверное число предупреждения!
+
+
+ Ты не можешь меня предупредить!
+
+
+ Ты не можешь предупредить владельца этого сервера!
+
+
+ Ты не можешь предупредить этого участника!
+
+
+ Ты не можешь себя предупредить!
+
+
+ Ты не можешь снять с меня предупреждения!
+
+
+ Ты не можешь снять предупреждения с владельца этого сервера!
+
+
+ Ты не можешь снять предупреждения с этого участника!
+
+
+ Ты не можешь снять с себя предупреждения!
+
+
+ Я не могу снимать предупреждения этого участника!
+
+
+ Я не могу предупредить этого участника!
+
+
+ Я не могу предупреждать участников этого сервера!
+
+
+ Ты не можешь просмотреть мои предупреждения!
+
+
+ Ты не можешь просмотреть предупреждения владельца этого сервера!
+
+
+ Ты не можешь просмотреть предупреждения этого участника!
+
+
+ Вместо этого, используйте эту команду без параметров.
+
+
+ Ты не можешь снимать предупреждения с участников этого сервера!
+
+
+ Ты не можешь предупреждать участников этого сервера!
+
+
+ Ты не можешь просматривать предупреждения участников этого сервера!
+
+
+ Предупреждения пользователя {0}:
+
+
+ Тип наказания: {0}
+
+
+ Превышен порог предупреждений. ({0})
+
+
+ Длительность наказания предупреждения не установлена для текущего типа наказания.
+
+
+ Увеличьте порог предупреждения или установите наказание за предупреждение.
+
+
+ Установлено неверное наказание за предупреждение.
diff --git a/TeamOctolings.Octobot/Program.cs b/TeamOctolings.Octobot/Program.cs
index 8cdbdcf..d1d6220 100644
--- a/TeamOctolings.Octobot/Program.cs
+++ b/TeamOctolings.Octobot/Program.cs
@@ -39,7 +39,8 @@ public sealed class Program
private static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
- .AddDiscordService(services =>
+ .AddDiscordService(
+ services =>
{
var configuration = services.GetRequiredService();
@@ -48,22 +49,25 @@ public sealed class Program
"No bot token has been provided. Set the "
+ "BOT_TOKEN environment variable to a valid token.");
}
- ).ConfigureServices((_, services) =>
+ ).ConfigureServices(
+ (_, services) =>
{
- services.Configure(options =>
- {
- options.Intents |= GatewayIntents.MessageContents
- | GatewayIntents.GuildMembers
- | GatewayIntents.GuildPresences
- | GatewayIntents.GuildScheduledEvents;
- });
- services.Configure(cSettings =>
- {
- cSettings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1));
- cSettings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30));
- cSettings.SetAbsoluteExpiration(TimeSpan.FromDays(7));
- cSettings.SetSlidingExpiration(TimeSpan.FromDays(7));
- });
+ services.Configure(
+ options =>
+ {
+ options.Intents |= GatewayIntents.MessageContents
+ | GatewayIntents.GuildMembers
+ | GatewayIntents.GuildPresences
+ | GatewayIntents.GuildScheduledEvents;
+ });
+ services.Configure(
+ cSettings =>
+ {
+ cSettings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1));
+ cSettings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30));
+ cSettings.SetAbsoluteExpiration(TimeSpan.FromDays(7));
+ cSettings.SetSlidingExpiration(TimeSpan.FromDays(7));
+ });
services.AddTransient()
// Init
@@ -83,13 +87,14 @@ public sealed class Program
.AddHostedService()
.AddHostedService();
}
- ).ConfigureLogging(c => c.AddConsole()
- .AddFile("Logs/Octobot-{Date}.log",
- outputTemplate: "{Timestamp:o} [{Level:u4}] {Message} {NewLine}{Exception}")
- .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning)
- .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning)
- .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning)
- .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning)
+ ).ConfigureLogging(
+ c => c.AddConsole()
+ .AddFile("Logs/Octobot-{Date}.log",
+ outputTemplate: "{Timestamp:o} [{Level:u4}] {Message} {NewLine}{Exception}")
+ .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning)
+ .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning)
+ .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning)
+ .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning)
);
}
}
diff --git a/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs
index b24ef0b..cebb1ea 100644
--- a/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs
+++ b/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs
@@ -94,7 +94,7 @@ public sealed class GuildLoadedResponder : IResponder
GuildSettings.PrivateFeedbackChannel.Get(cfg), embedResult: embed, ct: ct);
}
- private async Task SendDataLoadFailed(IGuild guild, GuildData data, IUser bot, CancellationToken ct = default)
+ private async Task SendDataLoadFailed(IGuild guild, GuildData data, IUser bot, CancellationToken ct)
{
var channelResult = await _utility.GetEmergencyFeedbackChannel(guild, data, ct);
if (!channelResult.IsDefined(out var channel))
@@ -120,6 +120,6 @@ public sealed class GuildLoadedResponder : IResponder
);
return await _channelApi.CreateMessageWithEmbedResultAsync(channel, embedResult: errorEmbed,
- components: new[] { new ActionRowComponent([issuesButton]) }, ct: ct);
+ components: new[] { new ActionRowComponent(new[] { issuesButton }) }, ct: ct);
}
}
diff --git a/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs b/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs
index ae9f174..c1f1da0 100644
--- a/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs
+++ b/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs
@@ -81,7 +81,7 @@ public sealed class GuildMemberJoinedResponder : IResponder
}
private async Task TryReturnRolesAsync(
- JsonNode cfg, MemberData memberData, Snowflake guildId, Snowflake userId, CancellationToken ct = default)
+ JsonNode cfg, MemberData memberData, Snowflake guildId, Snowflake userId, CancellationToken ct)
{
if (!GuildSettings.ReturnRolesOnRejoin.Get(cfg))
{
diff --git a/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs b/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs
index 957a107..9774899 100644
--- a/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs
+++ b/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs
@@ -36,9 +36,13 @@ public sealed class GuildMemberLeftResponder : IResponder
var cfg = data.Settings;
var memberData = data.GetOrCreateMemberData(user.ID);
- if (memberData.BannedUntil is not null || memberData.Kicked
- || GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty()
- || GuildSettings.LeaveMessage.Get(cfg) is "off" or "disable" or "disabled")
+ 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;
}
diff --git a/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs b/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs
index f0e3d22..88a8de2 100644
--- a/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs
+++ b/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs
@@ -66,10 +66,10 @@ public sealed class MessageDeletedResponder : IResponder
return ResultExtensions.FromError(auditLogResult);
}
- var deleterResult = Result.FromSuccess(message.Author);
+ var auditLog = auditLogPage.AuditLogEntries.Single();
- var auditLog = auditLogPage.AuditLogEntries.SingleOrDefault();
- if (auditLog is { UserID: not null }
+ var deleterResult = Result.FromSuccess(message.Author);
+ if (auditLog.UserID is not null
&& auditLog.Options.Value.ChannelID == gatewayEvent.ChannelID
&& DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2)
{
diff --git a/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs b/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs
index e3d1c58..2968562 100644
--- a/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs
+++ b/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs
@@ -36,29 +36,40 @@ public sealed class MessageEditedResponder : IResponder
public async Task RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default)
{
+ if (!gatewayEvent.ID.IsDefined(out var messageId))
+ {
+ return new ArgumentNullError(nameof(gatewayEvent.ID));
+ }
+
+ if (!gatewayEvent.ChannelID.IsDefined(out var channelId))
+ {
+ return new ArgumentNullError(nameof(gatewayEvent.ChannelID));
+ }
+
if (!gatewayEvent.GuildID.IsDefined(out var guildId)
- || !gatewayEvent.EditedTimestamp.HasValue
- || gatewayEvent.Author.IsBot.OrDefault(false))
+ || !gatewayEvent.Author.IsDefined(out var author)
+ || !gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)
+ || !gatewayEvent.Content.IsDefined(out var newContent))
{
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.Success;
}
- var cacheKey = new KeyHelpers.MessageCacheKey(gatewayEvent.ChannelID, gatewayEvent.ID);
+ var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId);
var messageResult = await _cacheService.TryGetValueAsync(
cacheKey, ct);
if (!messageResult.IsDefined(out var message))
{
- _ = _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct);
+ _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct);
return Result.Success;
}
- if (message.Content == gatewayEvent.Content)
+ if (message.Content == newContent)
{
return Result.Success;
}
@@ -72,22 +83,22 @@ public sealed class MessageEditedResponder : IResponder
// We don't need to await this since the result is not needed
// NOTE: Because this is not awaited, there may be a race condition depending on how fast clients are able to edit their messages
// NOTE: Awaiting this might not even solve this if the same responder is called asynchronously
- _ = _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct);
+ _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct);
- var diff = InlineDiffBuilder.Diff(message.Content, gatewayEvent.Content);
+ var diff = InlineDiffBuilder.Diff(message.Content, newContent);
Messages.Culture = GuildSettings.Language.Get(cfg);
var builder = new StringBuilder()
.AppendLine(diff.AsMarkdown())
.AppendLine(string.Format(Messages.DescriptionActionJumpToMessage,
- $"https://discord.com/channels/{guildId}/{gatewayEvent.ChannelID}/{gatewayEvent.ID}")
+ $"https://discord.com/channels/{guildId}/{channelId}/{messageId}")
);
var embed = new EmbedBuilder()
.WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author)
.WithDescription(builder.ToString())
- .WithTimestamp(gatewayEvent.EditedTimestamp.Value)
+ .WithTimestamp(timestamp.Value)
.WithColour(ColorsList.Yellow)
.Build();
diff --git a/TeamOctolings.Octobot/Services/AccessControlService.cs b/TeamOctolings.Octobot/Services/AccessControlService.cs
index d39c9e5..826983e 100644
--- a/TeamOctolings.Octobot/Services/AccessControlService.cs
+++ b/TeamOctolings.Octobot/Services/AccessControlService.cs
@@ -100,7 +100,8 @@ public sealed class AccessControlService
{
"Ban" => DiscordPermission.BanMembers,
"Kick" => DiscordPermission.KickMembers,
- "Mute" or "Unmute" => DiscordPermission.ModerateMembers,
+ "Mute" or "Unmute" or "Warn" or "Unwarn" or "GetWarns"
+ => DiscordPermission.ModerateMembers,
_ => throw new Exception()
});
diff --git a/TeamOctolings.Octobot/Services/GuildDataService.cs b/TeamOctolings.Octobot/Services/GuildDataService.cs
index 88edb5f..866ee08 100644
--- a/TeamOctolings.Octobot/Services/GuildDataService.cs
+++ b/TeamOctolings.Octobot/Services/GuildDataService.cs
@@ -27,7 +27,7 @@ public sealed class GuildDataService : BackgroundService
return SaveAsync(ct);
}
- private Task SaveAsync(CancellationToken ct = default)
+ private Task SaveAsync(CancellationToken ct)
{
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 = default)
+ private static async Task SerializeObjectSafelyAsync(T obj, string path, CancellationToken ct)
{
var tempFilePath = path + ".tmp";
await using (var tempFileStream = File.Create(tempFilePath))
@@ -75,48 +75,78 @@ public sealed class GuildDataService : BackgroundService
{
var path = $"GuildData/{guildId}";
var memberDataPath = $"{path}/MemberData";
-
var settingsPath = $"{path}/Settings.json";
-
var scheduledEventsPath = $"{path}/ScheduledEvents.json";
MigrateDataDirectory(guildId, path);
Directory.CreateDirectory(path);
+ if (!File.Exists(settingsPath))
+ {
+ await File.WriteAllTextAsync(settingsPath, "{}", ct);
+ }
+
+ if (!File.Exists(scheduledEventsPath))
+ {
+ await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct);
+ }
+
var dataLoadFailed = false;
- var jsonSettings = await LoadGuildSettings(settingsPath, ct);
+ await using var settingsStream = File.OpenRead(settingsPath);
+ JsonNode? jsonSettings = null;
+ try
+ {
+ jsonSettings = await JsonNode.ParseAsync(settingsStream, cancellationToken: ct);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Guild settings load failed: {Path}", settingsPath);
+ dataLoadFailed = true;
+ }
+
if (jsonSettings is not null)
{
FixJsonSettings(jsonSettings);
}
- else
- {
- dataLoadFailed = true;
- }
- var events = await LoadScheduledEvents(scheduledEventsPath, ct);
- if (events is null)
+ await using var eventsStream = File.OpenRead(scheduledEventsPath);
+ Dictionary? events = null;
+ try
{
+ events = await JsonSerializer.DeserializeAsync>(
+ eventsStream, cancellationToken: ct);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Guild scheduled events load failed: {Path}", scheduledEventsPath);
dataLoadFailed = true;
}
var memberData = new Dictionary();
- foreach (var dataFileInfo in Directory.CreateDirectory(memberDataPath).GetFiles()
- .Where(dataFileInfo =>
- !memberData.ContainsKey(
- ulong.Parse(dataFileInfo.Name.Replace(".json", "").Replace(".tmp", "")))))
+ foreach (var dataFileInfo in Directory.CreateDirectory(memberDataPath).GetFiles())
{
- var data = await LoadMemberData(dataFileInfo, memberDataPath, true, ct);
-
- if (data == null)
+ await using var dataStream = dataFileInfo.OpenRead();
+ MemberData? data;
+ try
{
+ data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Member data load failed: {MemberDataPath}/{FileName}", memberDataPath,
+ dataFileInfo.Name);
dataLoadFailed = true;
continue;
}
- memberData.TryAdd(data.Id, data);
+ if (data is null)
+ {
+ continue;
+ }
+
+ memberData.Add(data.Id, data);
}
var finalData = new GuildData(
@@ -130,133 +160,6 @@ public sealed class GuildDataService : BackgroundService
return finalData;
}
- private async Task LoadMemberData(FileInfo dataFileInfo, string memberDataPath, bool loadTmp,
- CancellationToken ct = default)
- {
- MemberData? data;
- var temporaryPath = $"{dataFileInfo.FullName}.tmp";
- var usedInfo = loadTmp && File.Exists(temporaryPath) ? new FileInfo(temporaryPath) : dataFileInfo;
-
- var isTmp = usedInfo.Extension is ".tmp";
- try
- {
- await using var dataStream = usedInfo.OpenRead();
- data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct);
- if (isTmp)
- {
- usedInfo.CopyTo(usedInfo.FullName.Replace(".tmp", ""), true);
- usedInfo.Delete();
- }
- }
- catch (Exception e)
- {
- if (isTmp)
- {
- _logger.LogWarning(e,
- "Unable to load temporary member data file, deleting: {MemberDataPath}/{FileName}", memberDataPath,
- usedInfo.Name);
- usedInfo.Delete();
- return await LoadMemberData(dataFileInfo, memberDataPath, false, ct);
- }
-
- _logger.LogError(e, "Member data load failed: {MemberDataPath}/{FileName}", memberDataPath,
- usedInfo.Name);
- return null;
- }
-
- return data;
- }
-
- private async Task?> LoadScheduledEvents(string scheduledEventsPath,
- CancellationToken ct = default)
- {
- var tempScheduledEventsPath = $"{scheduledEventsPath}.tmp";
-
- if (!File.Exists(scheduledEventsPath) && !File.Exists(tempScheduledEventsPath))
- {
- return new Dictionary();
- }
-
- if (File.Exists(tempScheduledEventsPath))
- {
- _logger.LogWarning("Found temporary scheduled events file, will try to parse and copy to main: ${Path}",
- tempScheduledEventsPath);
- try
- {
- await using var tempEventsStream = File.OpenRead(tempScheduledEventsPath);
- var events = await JsonSerializer.DeserializeAsync>(
- tempEventsStream, cancellationToken: ct);
- File.Copy(tempScheduledEventsPath, scheduledEventsPath, true);
- File.Delete(tempScheduledEventsPath);
-
- _logger.LogInformation("Successfully loaded temporary scheduled events file: ${Path}",
- tempScheduledEventsPath);
- return events;
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Unable to load temporary scheduled events file: {Path}, deleting",
- tempScheduledEventsPath);
- File.Delete(tempScheduledEventsPath);
- }
- }
-
- try
- {
- await using var eventsStream = File.OpenRead(scheduledEventsPath);
- return await JsonSerializer.DeserializeAsync>(
- eventsStream, cancellationToken: ct);
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Guild scheduled events load failed: {Path}", scheduledEventsPath);
- return null;
- }
- }
-
- private async Task LoadGuildSettings(string settingsPath, CancellationToken ct = default)
- {
- var tempSettingsPath = $"{settingsPath}.tmp";
-
- if (!File.Exists(settingsPath) && !File.Exists(tempSettingsPath))
- {
- return new JsonObject();
- }
-
- if (File.Exists(tempSettingsPath))
- {
- _logger.LogWarning("Found temporary settings file, will try to parse and copy to main: ${Path}",
- tempSettingsPath);
- try
- {
- await using var tempSettingsStream = File.OpenRead(tempSettingsPath);
- var jsonSettings = await JsonNode.ParseAsync(tempSettingsStream, cancellationToken: ct);
-
- File.Copy(tempSettingsPath, settingsPath, true);
- File.Delete(tempSettingsPath);
-
- _logger.LogInformation("Successfully loaded temporary settings file: ${Path}", tempSettingsPath);
- return jsonSettings;
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Unable to load temporary settings file: {Path}, deleting", tempSettingsPath);
- File.Delete(tempSettingsPath);
- }
- }
-
- try
- {
- await using var settingsStream = File.OpenRead(settingsPath);
- return await JsonNode.ParseAsync(settingsStream, cancellationToken: ct);
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Guild settings load failed: {Path}", settingsPath);
- return null;
- }
- }
-
private void MigrateDataDirectory(Snowflake guildId, string newPath)
{
var oldPath = $"{guildId}";
diff --git a/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs b/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs
index 3170060..51cf647 100644
--- a/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs
+++ b/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs
@@ -62,7 +62,7 @@ public sealed partial class MemberUpdateService : BackgroundService
}
}
- private async Task TickMemberDatasAsync(Snowflake guildId, CancellationToken ct = default)
+ private async Task TickMemberDatasAsync(Snowflake guildId, CancellationToken ct)
{
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 = default)
+ CancellationToken ct)
{
var failedResults = new List();
var id = data.Id.ToSnowflake();
@@ -144,7 +144,7 @@ public sealed partial class MemberUpdateService : BackgroundService
}
private async Task TryAutoUnbanAsync(
- Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct = default)
+ Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct)
{
if (data.BannedUntil is null || DateTimeOffset.UtcNow <= data.BannedUntil)
{
@@ -169,7 +169,7 @@ public sealed partial class MemberUpdateService : BackgroundService
}
private async Task TryAutoUnmuteAsync(
- Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct = default)
+ Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct)
{
if (data.MutedUntil is null || DateTimeOffset.UtcNow <= data.MutedUntil)
{
@@ -188,7 +188,7 @@ public sealed partial class MemberUpdateService : BackgroundService
}
private async Task FilterNicknameAsync(Snowflake guildId, IUser user, IGuildMember member,
- CancellationToken ct = default)
+ CancellationToken ct)
{
var currentNickname = member.Nickname.IsDefined(out var nickname)
? nickname
@@ -226,7 +226,7 @@ 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 = default)
+ CancellationToken ct)
{
if (DateTimeOffset.UtcNow < reminder.At)
{
@@ -234,7 +234,7 @@ public sealed partial class MemberUpdateService : BackgroundService
}
var builder = new StringBuilder()
- .AppendLine(MarkdownExtensions.Quote(reminder.Text))
+ .AppendBulletPointLine(string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text)))
.AppendBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage,
$"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}"));
diff --git a/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs b/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs
index 389a6a8..ce9c212 100644
--- a/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs
+++ b/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs
@@ -46,7 +46,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
}
}
- private async Task TickScheduledEventsAsync(Snowflake guildId, CancellationToken ct = default)
+ private async Task TickScheduledEventsAsync(Snowflake guildId, CancellationToken ct)
{
var failedResults = new List();
var data = await _guildData.GetData(guildId, ct);
@@ -133,7 +133,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
private async Task TickScheduledEventAsync(
Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData,
- CancellationToken ct = default)
+ CancellationToken ct)
{
if (GuildSettings.AutoStartEvents.Get(data.Settings)
&& DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime
@@ -160,7 +160,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
}
private async Task AutoStartEventAsync(
- Snowflake guildId, IGuildScheduledEvent scheduledEvent, CancellationToken ct = default)
+ Snowflake guildId, IGuildScheduledEvent scheduledEvent, CancellationToken ct)
{
return (Result)await _eventApi.ModifyGuildScheduledEventAsync(
guildId, scheduledEvent.ID,
@@ -229,7 +229,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
return await _channelApi.CreateMessageWithEmbedResultAsync(
GuildSettings.EventNotificationChannel.Get(settings), roleMention, embedResult: embed,
- components: new[] { new ActionRowComponent([button]) }, ct: ct);
+ components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct);
}
private static Result GetExternalScheduledEventCreatedEmbedDescription(
@@ -319,7 +319,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
}
private async Task SendScheduledEventCompletedMessage(ScheduledEventData eventData, GuildData data,
- CancellationToken ct = default)
+ CancellationToken ct)
{
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
{
@@ -351,7 +351,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
}
private async Task SendScheduledEventCancelledMessage(ScheduledEventData eventData, GuildData data,
- CancellationToken ct = default)
+ CancellationToken ct)
{
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
{
@@ -405,7 +405,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService
}
private async Task SendEarlyEventNotificationAsync(
- IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default)
+ IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct)
{
if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty())
{
diff --git a/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs b/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs
index 8eaa4c2..b07256f 100644
--- a/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs
+++ b/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs
@@ -29,11 +29,7 @@ public sealed class SongUpdateService : BackgroundService
("Callie", "Bomb Rush Blush", new TimeSpan(0, 2, 18)),
("Turquoise October", "Octoling Rendezvous", new TimeSpan(0, 1, 57)),
("Damp Socks feat. Off the Hook", "Tentacle to the Metal", new TimeSpan(0, 2, 51)),
- ("Off the Hook feat. Dedf1sh", "Spectrum Obligato ~ Ebb & Flow (Out of Order)", new TimeSpan(0, 4, 30)),
- ("Dedf1sh feat. Off the Hook", "#47 onward", new TimeSpan(0, 4, 40)),
- ("Free Association", "EchΘ Θnslaught", new TimeSpan(0, 2, 52)),
- ("Off the Hook", "Short Order", new TimeSpan(0, 3, 36)),
- ("Deep Cut", "Fins in the Air", new TimeSpan(0, 3, 1))
+ ("Off the Hook", "Fly Octo Fly ~ Ebb & Flow (Octo)", new TimeSpan(0, 3, 5))
];
private static readonly (string Author, string Name, TimeSpan Duration)[] SpecialSongList =
@@ -41,7 +37,7 @@ public sealed class SongUpdateService : BackgroundService
("Squid Sisters", "Maritime Memory", new TimeSpan(0, 2, 47))
];
- private readonly List _activityList = [new("with Remora.Discord", ActivityType.Game)];
+ private readonly List _activityList = [new Activity("with Remora.Discord", ActivityType.Game)];
private readonly DiscordGatewayClient _client;
private readonly GuildDataService _guildData;
diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj
index b67eaf8..19e37f9 100644
--- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj
+++ b/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj
@@ -2,7 +2,7 @@
Exe
- net9.0
+ net8.0
enable
enable
2.0.0
@@ -24,14 +24,14 @@
-
+
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/TeamOctolings.Octobot/Utility.cs b/TeamOctolings.Octobot/Utility.cs
index a2f7aca..463212b 100644
--- a/TeamOctolings.Octobot/Utility.cs
+++ b/TeamOctolings.Octobot/Utility.cs
@@ -67,8 +67,8 @@ public sealed class Utility
builder.Append($"{Mention.Role(role)} ");
}
- builder = subscribers.Where(subscriber =>
- !data.GetOrCreateMemberData(subscriber.User.ID).Roles.Contains(role.Value))
+ builder = subscribers.Where(
+ subscriber => !data.GetOrCreateMemberData(subscriber.User.ID).Roles.Contains(role.Value))
.Aggregate(builder, (current, subscriber) => current.Append($"{Mention.User(subscriber.User)} "));
return builder.ToString();
}
@@ -125,7 +125,7 @@ public sealed class Utility
}
}
- public async Task> GetEmergencyFeedbackChannel(IGuild guild, GuildData data, CancellationToken ct = default)
+ public async Task> GetEmergencyFeedbackChannel(IGuild guild, GuildData data, CancellationToken ct)
{
var privateFeedback = GuildSettings.PrivateFeedbackChannel.Get(data.Settings);
if (!privateFeedback.Empty())
diff --git a/compose.example.yaml b/compose.example.yaml
deleted file mode 100644
index 522281f..0000000
--- a/compose.example.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-services:
- octobot:
- container_name: octobot
- build:
- context: .
- args:
- - PUBLISH_OPTIONS
- environment:
- - BOT_TOKEN
- volumes:
- - guild-data:/Octobot/GuildData
- - logs:/Octobot/Logs
- restart: unless-stopped
-
-volumes:
- guild-data:
- logs:
diff --git a/docs/README.md b/docs/README.md
index ccc3b83..7056857 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -15,16 +15,23 @@ 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 Inkantation!
+* Listen to music!
*...a-a-and more!*
## Building Octobot
-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) |
-| --- | --- |
+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'
+```
## Contributing