mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-04-19 16:33:36 +03:00
Merge branch 'master' into left-guild-message
Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com>
This commit is contained in:
commit
196f65c1a4
32 changed files with 774 additions and 259 deletions
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
|
@ -1,2 +1,2 @@
|
|||
* @LabsDevelopment/octobot
|
||||
/docs/ @LabsDevelopment/octobot-docs
|
||||
* @TeamOctolings/octobot
|
||||
/docs/ @TeamOctolings/octobot-docs
|
||||
|
|
2
.github/workflows/build-pr.yml
vendored
2
.github/workflows/build-pr.yml
vendored
|
@ -23,7 +23,7 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
|
||||
- name: ReSharper CLI InspectCode
|
||||
uses: muno92/resharper_inspectcode@1.11.1
|
||||
uses: muno92/resharper_inspectcode@1.11.7
|
||||
with:
|
||||
solutionPath: ./Octobot.sln
|
||||
ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor
|
||||
|
|
|
@ -18,3 +18,5 @@ P:System.DateTime.Now;Use System.DateTime.UtcNow instead.
|
|||
P:System.DateTimeOffset.Now;Use System.DateTimeOffset.UtcNow instead.
|
||||
P:System.DateTimeOffset.DateTime;Use System.DateTimeOffset.UtcDateTime instead.
|
||||
M:System.IO.File.OpenWrite(System.String);File.OpenWrite(string) does not clear the file before writing to it. Use File.Create(string) instead.
|
||||
M:System.Threading.Thread.Sleep(System.Int32);Use Task.Delay(int, CancellationToken) instead.
|
||||
M:System.Threading.Thread.Sleep(System.TimeSpan);Use Task.Delay(TimeSpan, CancellationToken) instead.
|
||||
|
|
|
@ -9,11 +9,11 @@
|
|||
<Title>Octobot</Title>
|
||||
<Authors>Octol1ttle, mctaylors, neroduckale</Authors>
|
||||
<Copyright>AGPLv3</Copyright>
|
||||
<PackageProjectUrl>https://github.com/LabsDevelopment/Octobot</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://github.com/LabsDevelopment/Octobot/blob/master/LICENSE</PackageLicenseUrl>
|
||||
<RepositoryUrl>https://github.com/LabsDevelopment/Octobot</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/TeamOctolings/Octobot</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://github.com/TeamOctolings/Octobot/blob/master/LICENSE</PackageLicenseUrl>
|
||||
<RepositoryUrl>https://github.com/TeamOctolings/Octobot</RepositoryUrl>
|
||||
<RepositoryType>github</RepositoryType>
|
||||
<Company>LabsDevelopment</Company>
|
||||
<Company>TeamOctolings</Company>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<Description>A general-purpose Discord bot for moderation written in C#</Description>
|
||||
<ApplicationIcon>docs/octobot.ico</ApplicationIcon>
|
||||
|
@ -26,10 +26,10 @@
|
|||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Remora.Commands" Version="10.0.5" />
|
||||
<PackageReference Include="Remora.Discord.Caching" Version="37.0.0" />
|
||||
<PackageReference Include="Remora.Discord.Extensions" Version="5.3.2" />
|
||||
<PackageReference Include="Remora.Discord.Hosting" Version="6.0.7" />
|
||||
<PackageReference Include="Remora.Discord.Interactivity" Version="4.5.1" />
|
||||
<PackageReference Include="Remora.Discord.Caching" Version="38.0.1" />
|
||||
<PackageReference Include="Remora.Discord.Extensions" Version="5.3.4" />
|
||||
<PackageReference Include="Remora.Discord.Hosting" Version="6.0.9" />
|
||||
<PackageReference Include="Remora.Discord.Interactivity" Version="4.5.3" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
|
|
@ -29,7 +29,7 @@ While pull requests from unaffiliated contributors are welcome, please note that
|
|||
internal issues that haven't been published to the issue tracker yet. Reviewing PRs is done on a best-effort basis, so
|
||||
please be aware that it may take a while before a core maintainer gets around to review your change.
|
||||
|
||||
The [issue tracker](https://github.com/LabsDevelopment/Octobot/issues) should provide plenty of issues to start with.
|
||||
The [issue tracker](https://github.com/TeamOctolings/Octobot/issues) should provide plenty of issues to start with.
|
||||
Make sure to check that an issue you're planning to resolve does not already have people working on it and that there
|
||||
are no PRs associated with it
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<p align="center">
|
||||
<img src="https://cdn.mctaylors.ru/octobot-banner.png" alt="Octobot banner"/>
|
||||
<img src="octobot-banner.png" alt="Octobot banner"/>
|
||||
</p>
|
||||
|
||||
<a href="https://github.com/LabsDevelopment/Octobot/blob/master/LICENSE"><img src="https://img.shields.io/github/license/LabsDevelopment/Octobot?logo=git"></img></a>
|
||||
<a href="https://github.com/TeamOctolings/Octobot/blob/master/LICENSE"><img src="https://img.shields.io/github/license/TeamOctolings/Octobot?logo=git"></img></a>
|
||||
<a href="https://github.com/Remora/Remora.Discord"><img src="https://img.shields.io/badge/powered_by-Remora.Discord-blue"></img></a>
|
||||
<a href="https://github.com/LabsDevelopment/Octobot/commit/master"><img src="https://img.shields.io/github/last-commit/LabsDevelopment/Octobot?logo=github"></img></a>
|
||||
<a href="https://github.com/TeamOctolings/Octobot/commit/master"><img src="https://img.shields.io/github/last-commit/TeamOctolings/Octobot?logo=github"></img></a>
|
||||
|
||||
Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) written by [Labs Development Team](https://github.com/LabsDevelopment) in C# and Remora.Discord
|
||||
Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) written by [Team Octolings](https://github.com/TeamOctolings) in C# and Remora.Discord
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -19,25 +19,13 @@ Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) wr
|
|||
|
||||
*...a-a-and more!*
|
||||
|
||||
[//]: # (if you are reading this, message @mctaylors and ask him to bring back the wiki)
|
||||
|
||||
## Invite Octobot <a href="https://github.com/LabsDevelopment/Octobot/deployments/production"><img src="https://img.shields.io/github/actions/workflow/status/LabsDevelopment/Octobot/.github/workflows/build-push.yml?logo=github&label=production"></img></a>
|
||||
|
||||
Did you know that Octobot is a public bot? You can invite it to your server and use it without building it!
|
||||
<p align="center">
|
||||
<a href="https://discord.com/api/oauth2/authorize?client_id=855023234407333888&permissions=1383382133894&scope=bot%20applications.commands"><img src="https://cdn.mctaylors.ru/discord-add-app.png"></img></a>
|
||||
</p>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The bot will not be able to respond in private channels unless you have configured permissions for the bot in those channels.
|
||||
|
||||
## Building Octobot
|
||||
|
||||
1. Install [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
|
||||
2. Go to the [Discord Developer Portal](https://discord.com/developers), create a new application and get a bot token. Don't forget to also enable all intents!
|
||||
3. Clone this repository and open `Octobot` folder.
|
||||
```
|
||||
git clone https://github.com/LabsDevelopment/Octobot
|
||||
git clone https://github.com/TeamOctolings/Octobot
|
||||
cd Octobot
|
||||
```
|
||||
4. Run Octobot using `dotnet` with `BOT_TOKEN` variable.
|
||||
|
|
BIN
docs/octobot-banner.png
Normal file
BIN
docs/octobot-banner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 120 KiB |
|
@ -591,7 +591,79 @@
|
|||
<data name="SettingsLeaveMessage" xml:space="preserve">
|
||||
<value>Leave message</value>
|
||||
</data>
|
||||
<data name="InvalidTimeSpan" xml:space="preserve">
|
||||
<value>Time specified incorrectly!</value>
|
||||
</data>
|
||||
<data name="UserInfoKicked" xml:space="preserve">
|
||||
<value>Kicked</value>
|
||||
</data>
|
||||
<data name="ReminderEdited" xml:space="preserve">
|
||||
<value>Reminder edited</value>
|
||||
</data>
|
||||
<data name="EightBallPositive1" xml:space="preserve">
|
||||
<value>It is certain</value>
|
||||
</data>
|
||||
<data name="EightBallPositive2" xml:space="preserve">
|
||||
<value>It is decidedly so</value>
|
||||
</data>
|
||||
<data name="EightBallPositive3" xml:space="preserve">
|
||||
<value>Without a doubt</value>
|
||||
</data>
|
||||
<data name="EightBallPositive4" xml:space="preserve">
|
||||
<value>Yes — definitely</value>
|
||||
</data>
|
||||
<data name="EightBallPositive5" xml:space="preserve">
|
||||
<value>You may rely on it</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable1" xml:space="preserve">
|
||||
<value>As I see it, yes</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable2" xml:space="preserve">
|
||||
<value>Most likely</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable3" xml:space="preserve">
|
||||
<value>Outlook good</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable4" xml:space="preserve">
|
||||
<value>Signs point to yes</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable5" xml:space="preserve">
|
||||
<value>Yes</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral1" xml:space="preserve">
|
||||
<value>Reply hazy, try again</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral2" xml:space="preserve">
|
||||
<value>Ask again later</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral3" xml:space="preserve">
|
||||
<value>Better not tell you now</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral4" xml:space="preserve">
|
||||
<value>Cannot predict now</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral5" xml:space="preserve">
|
||||
<value>Concentrate and ask again</value>
|
||||
</data>
|
||||
<data name="EightBallNegative1" xml:space="preserve">
|
||||
<value>Don’t count on it</value>
|
||||
</data>
|
||||
<data name="EightBallNegative2" xml:space="preserve">
|
||||
<value>My reply is no</value>
|
||||
</data>
|
||||
<data name="EightBallNegative3" xml:space="preserve">
|
||||
<value>My sources say no</value>
|
||||
</data>
|
||||
<data name="EightBallNegative4" xml:space="preserve">
|
||||
<value>Outlook not so good</value>
|
||||
</data>
|
||||
<data name="EightBallNegative5" xml:space="preserve">
|
||||
<value>Very doubtful</value>
|
||||
</data>
|
||||
<data name="TimeSpanExample" xml:space="preserve">
|
||||
<value>Example of a valid input: `1h30m`</value>
|
||||
</data>
|
||||
<data name="SettingsWelcomeMessagesChannel" xml:space="preserve">
|
||||
<value>Welcome messages channel</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
@ -591,7 +591,79 @@
|
|||
<data name="SettingsLeaveMessage" xml:space="preserve">
|
||||
<value>Сообщение о выходе</value>
|
||||
</data>
|
||||
<data name="InvalidTimeSpan" xml:space="preserve">
|
||||
<value>Неправильно указано время!</value>
|
||||
</data>
|
||||
<data name="UserInfoKicked" xml:space="preserve">
|
||||
<value>Выгнан</value>
|
||||
</data>
|
||||
<data name="ReminderEdited" xml:space="preserve">
|
||||
<value>Напоминание отредактировано</value>
|
||||
</data>
|
||||
<data name="EightBallPositive1" xml:space="preserve">
|
||||
<value>Бесспорно</value>
|
||||
</data>
|
||||
<data name="EightBallPositive2" xml:space="preserve">
|
||||
<value>Предрешено</value>
|
||||
</data>
|
||||
<data name="EightBallPositive3" xml:space="preserve">
|
||||
<value>Никаких сомнений</value>
|
||||
</data>
|
||||
<data name="EightBallPositive4" xml:space="preserve">
|
||||
<value>Определённо да</value>
|
||||
</data>
|
||||
<data name="EightBallPositive5" xml:space="preserve">
|
||||
<value>Можешь быть уверен в этом</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable1" xml:space="preserve">
|
||||
<value>Мне кажется — «да»</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable2" xml:space="preserve">
|
||||
<value>Вероятнее всего</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable3" xml:space="preserve">
|
||||
<value>Хорошие перспективы</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable4" xml:space="preserve">
|
||||
<value>Знаки говорят — «да»</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable5" xml:space="preserve">
|
||||
<value>Да</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral1" xml:space="preserve">
|
||||
<value>Пока не ясно, попробуй снова</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral2" xml:space="preserve">
|
||||
<value>Спроси позже</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral3" xml:space="preserve">
|
||||
<value>Лучше не рассказывать</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral4" xml:space="preserve">
|
||||
<value>Сейчас нельзя предсказать</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral5" xml:space="preserve">
|
||||
<value>Сконцентрируйся и спроси снова</value>
|
||||
</data>
|
||||
<data name="EightBallNegative1" xml:space="preserve">
|
||||
<value>Даже не думай</value>
|
||||
</data>
|
||||
<data name="EightBallNegative2" xml:space="preserve">
|
||||
<value>Мой ответ — «нет»</value>
|
||||
</data>
|
||||
<data name="EightBallNegative3" xml:space="preserve">
|
||||
<value>По моим данным — «нет»</value>
|
||||
</data>
|
||||
<data name="EightBallNegative4" xml:space="preserve">
|
||||
<value>Перспективы не очень хорошие</value>
|
||||
</data>
|
||||
<data name="EightBallNegative5" xml:space="preserve">
|
||||
<value>Весьма сомнительно</value>
|
||||
</data>
|
||||
<data name="TimeSpanExample" xml:space="preserve">
|
||||
<value>Пример правильного ввода: `1ч30м`</value>
|
||||
</data>
|
||||
<data name="SettingsWelcomeMessagesChannel" xml:space="preserve">
|
||||
<value>Канал для приветствий</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
@ -591,7 +591,79 @@
|
|||
<data name="SettingsLeaveMessage" xml:space="preserve">
|
||||
<value>до свидания (типо настройка)</value>
|
||||
</data>
|
||||
<data name="InvalidTimeSpan" xml:space="preserve">
|
||||
<value>ты там правильно напиши таймспан</value>
|
||||
</data>
|
||||
<data name="UserInfoKicked" xml:space="preserve">
|
||||
<value>кикнут</value>
|
||||
</data>
|
||||
<data name="ReminderEdited" xml:space="preserve">
|
||||
<value>напоминалка подправлена</value>
|
||||
</data>
|
||||
<data name="EightBallPositive1" xml:space="preserve">
|
||||
<value>абсолютли</value>
|
||||
</data>
|
||||
<data name="EightBallPositive2" xml:space="preserve">
|
||||
<value>заявлено</value>
|
||||
</data>
|
||||
<data name="EightBallPositive3" xml:space="preserve">
|
||||
<value>ваще не сомневайся</value>
|
||||
</data>
|
||||
<data name="EightBallPositive4" xml:space="preserve">
|
||||
<value>100% да</value>
|
||||
</data>
|
||||
<data name="EightBallPositive5" xml:space="preserve">
|
||||
<value>будь в этом уверен</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable1" xml:space="preserve">
|
||||
<value>я считаю что да</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable2" xml:space="preserve">
|
||||
<value>ну вполне вероятно</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable3" xml:space="preserve">
|
||||
<value>ну выглядит нормально</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable4" xml:space="preserve">
|
||||
<value>мне сказали ок</value>
|
||||
</data>
|
||||
<data name="EightBallQuestionable5" xml:space="preserve">
|
||||
<value>мгм</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral1" xml:space="preserve">
|
||||
<value>ну-ка попробуй снова</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral2" xml:space="preserve">
|
||||
<value>давай позже</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral3" xml:space="preserve">
|
||||
<value>щас пока не скажу</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral4" xml:space="preserve">
|
||||
<value>я не могу сейчас предсказать</value>
|
||||
</data>
|
||||
<data name="EightBallNeutral5" xml:space="preserve">
|
||||
<value>ну сконцентрируйся и давай еще раз</value>
|
||||
</data>
|
||||
<data name="EightBallNegative1" xml:space="preserve">
|
||||
<value>даже не думай</value>
|
||||
</data>
|
||||
<data name="EightBallNegative2" xml:space="preserve">
|
||||
<value>мое завление это нет</value>
|
||||
</data>
|
||||
<data name="EightBallNegative3" xml:space="preserve">
|
||||
<value>я тут посчитал, короче нет</value>
|
||||
</data>
|
||||
<data name="EightBallNegative4" xml:space="preserve">
|
||||
<value>выглядит такое себе</value>
|
||||
</data>
|
||||
<data name="EightBallNegative5" xml:space="preserve">
|
||||
<value>чот сомневаюсь</value>
|
||||
</data>
|
||||
<data name="TimeSpanExample" xml:space="preserve">
|
||||
<value>правильно пишут так: `1h30m`</value>
|
||||
</data>
|
||||
<data name="SettingsWelcomeMessagesChannel" xml:space="preserve">
|
||||
<value>канал куда говорить здравствуйте</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
@ -100,7 +100,7 @@ public class AboutCommandGroup : CommandGroup
|
|||
.WithSmallTitle(string.Format(Messages.AboutBot, bot.Username), bot)
|
||||
.WithDescription(builder.ToString())
|
||||
.WithColour(ColorsList.Cyan)
|
||||
.WithImageUrl("https://cdn.mctaylors.ru/octobot-banner.png")
|
||||
.WithImageUrl("https://i.ibb.co/fS6wZhh/octobot-banner.png")
|
||||
.Build();
|
||||
|
||||
var repositoryButton = new ButtonComponent(
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Text;
|
|||
using JetBrains.Annotations;
|
||||
using Octobot.Data;
|
||||
using Octobot.Extensions;
|
||||
using Octobot.Parsers;
|
||||
using Octobot.Services;
|
||||
using Octobot.Services.Update;
|
||||
using Remora.Commands.Attributes;
|
||||
|
@ -75,7 +76,8 @@ public class BanCommandGroup : CommandGroup
|
|||
[Description("User to ban")] IUser target,
|
||||
[Description("Ban reason")] [MaxLength(256)]
|
||||
string reason,
|
||||
[Description("Ban duration")] TimeSpan? duration = null)
|
||||
[Description("Ban duration (e.g. 1h30m)")]
|
||||
string? duration = null)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId))
|
||||
{
|
||||
|
@ -104,7 +106,25 @@ public class BanCommandGroup : CommandGroup
|
|||
var data = await _guildData.GetData(guild.ID, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
return await BanUserAsync(executor, target, reason, duration, guild, data, channelId, bot, CancellationToken);
|
||||
if (duration is null)
|
||||
{
|
||||
return await BanUserAsync(executor, target, reason, null, guild, data, channelId, bot,
|
||||
CancellationToken);
|
||||
}
|
||||
|
||||
var parseResult = TimeSpanParser.TryParse(duration);
|
||||
if (!parseResult.IsDefined(out var timeSpan))
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder()
|
||||
.WithSmallTitle(Messages.InvalidTimeSpan, bot)
|
||||
.WithDescription(Messages.TimeSpanExample)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken);
|
||||
}
|
||||
|
||||
return await BanUserAsync(executor, target, reason, timeSpan, guild, data, channelId, bot, CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> BanUserAsync(
|
||||
|
@ -178,12 +198,8 @@ public class BanCommandGroup : CommandGroup
|
|||
title, target)
|
||||
.WithColour(ColorsList.Green).Build();
|
||||
|
||||
var logResult = _utility.LogActionAsync(
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct);
|
||||
if (!logResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(logResult.Error);
|
||||
}
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
@ -269,12 +285,9 @@ public class BanCommandGroup : CommandGroup
|
|||
|
||||
var title = string.Format(Messages.UserUnbanned, target.GetTag());
|
||||
var description = new StringBuilder().AppendBulletPoint(string.Format(Messages.DescriptionActionReason, reason));
|
||||
var logResult = _utility.LogActionAsync(
|
||||
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description.ToString(), target, ColorsList.Green, ct: ct);
|
||||
if (!logResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(logResult.Error);
|
||||
}
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
|
|
@ -136,12 +136,8 @@ public class ClearCommandGroup : CommandGroup
|
|||
return Result.FromError(deleteResult.Error);
|
||||
}
|
||||
|
||||
var logResult = _utility.LogActionAsync(
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description, bot, ColorsList.Red, false, ct);
|
||||
if (!logResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(logResult.Error);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(title, bot)
|
||||
.WithColour(ColorsList.Green).Build();
|
||||
|
|
|
@ -157,12 +157,9 @@ public class KickCommandGroup : CommandGroup
|
|||
|
||||
var title = string.Format(Messages.UserKicked, target.GetTag());
|
||||
var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason));
|
||||
var logResult = _utility.LogActionAsync(
|
||||
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct);
|
||||
if (!logResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(logResult.Error);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.UserKicked, target.GetTag()), target)
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Text;
|
|||
using JetBrains.Annotations;
|
||||
using Octobot.Data;
|
||||
using Octobot.Extensions;
|
||||
using Octobot.Parsers;
|
||||
using Octobot.Services;
|
||||
using Octobot.Services.Update;
|
||||
using Remora.Commands.Attributes;
|
||||
|
@ -50,7 +51,7 @@ public class MuteCommandGroup : CommandGroup
|
|||
/// A slash command that mutes a Discord member with the specified reason.
|
||||
/// </summary>
|
||||
/// <param name="target">The member to mute.</param>
|
||||
/// <param name="duration">The duration for this mute. The member will be automatically unmuted after this duration.</param>
|
||||
/// <param name="stringDuration">The duration for this mute. The member will be automatically unmuted after this duration.</param>
|
||||
/// <param name="reason">
|
||||
/// The reason for this mute. Must be encoded with <see cref="StringExtensions.EncodeHeader" /> when passed to
|
||||
/// <see cref="IDiscordRestGuildAPI.ModifyGuildMemberAsync" />.
|
||||
|
@ -72,7 +73,8 @@ public class MuteCommandGroup : CommandGroup
|
|||
[Description("Member to mute")] IUser target,
|
||||
[Description("Mute reason")] [MaxLength(256)]
|
||||
string reason,
|
||||
[Description("Mute duration")] TimeSpan duration)
|
||||
[Description("Mute duration (e.g. 1h30m)")] [Option("duration")]
|
||||
string stringDuration)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId))
|
||||
{
|
||||
|
@ -104,6 +106,18 @@ public class MuteCommandGroup : CommandGroup
|
|||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: CancellationToken);
|
||||
}
|
||||
|
||||
var parseResult = TimeSpanParser.TryParse(stringDuration);
|
||||
if (!parseResult.IsDefined(out var duration))
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder()
|
||||
.WithSmallTitle(Messages.InvalidTimeSpan, bot)
|
||||
.WithDescription(Messages.TimeSpanExample)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken);
|
||||
}
|
||||
|
||||
return await MuteUserAsync(executor, target, reason, duration, guildId, data, channelId, bot, CancellationToken);
|
||||
}
|
||||
|
||||
|
@ -140,12 +154,8 @@ public class MuteCommandGroup : CommandGroup
|
|||
.AppendBulletPoint(string.Format(
|
||||
Messages.DescriptionActionExpiresAt, Markdown.Timestamp(until))).ToString();
|
||||
|
||||
var logResult = _utility.LogActionAsync(
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description, target, ColorsList.Red, ct: ct);
|
||||
if (!logResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(logResult.Error);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.UserMuted, target.GetTag()), target)
|
||||
|
@ -326,12 +336,9 @@ public class MuteCommandGroup : CommandGroup
|
|||
|
||||
var title = string.Format(Messages.UserUnmuted, target.GetTag());
|
||||
var description = MarkdownExtensions.BulletPoint(string.Format(Messages.DescriptionActionReason, reason));
|
||||
var logResult = _utility.LogActionAsync(
|
||||
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description, target, ColorsList.Green, ct: ct);
|
||||
if (!logResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(logResult.Error);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(
|
||||
string.Format(Messages.UserUnmuted, target.GetTag()), target)
|
||||
|
|
|
@ -17,6 +17,7 @@ using Remora.Discord.Extensions.Embeds;
|
|||
using Remora.Discord.Extensions.Formatting;
|
||||
using Remora.Rest.Core;
|
||||
using Remora.Results;
|
||||
using Octobot.Parsers;
|
||||
|
||||
namespace Octobot.Commands;
|
||||
|
||||
|
@ -110,7 +111,7 @@ public class RemindCommandGroup : CommandGroup
|
|||
/// <summary>
|
||||
/// A slash command that schedules a reminder with the specified text.
|
||||
/// </summary>
|
||||
/// <param name="in">The period of time which must pass before the reminder will be sent.</param>
|
||||
/// <param name="timeSpanString">The period of time which must pass before the reminder will be sent.</param>
|
||||
/// <param name="text">The text of the reminder.</param>
|
||||
/// <returns>A feedback sending result which may or may not have succeeded.</returns>
|
||||
[Command("remind")]
|
||||
|
@ -119,8 +120,9 @@ public class RemindCommandGroup : CommandGroup
|
|||
[RequireContext(ChannelContext.Guild)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteReminderAsync(
|
||||
[Description("After what period of time mention the reminder")]
|
||||
TimeSpan @in,
|
||||
[Description("After what period of time mention the reminder (e.g. 1h30m)")]
|
||||
[Option("in")]
|
||||
string timeSpanString,
|
||||
[Description("Reminder text")] [MaxLength(512)]
|
||||
string text)
|
||||
{
|
||||
|
@ -129,6 +131,12 @@ public class RemindCommandGroup : CommandGroup
|
|||
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))
|
||||
{
|
||||
|
@ -138,14 +146,26 @@ public class RemindCommandGroup : CommandGroup
|
|||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
return await AddReminderAsync(@in, text, data, channelId, executor, CancellationToken);
|
||||
var parseResult = TimeSpanParser.TryParse(timeSpanString);
|
||||
if (!parseResult.IsDefined(out var timeSpan))
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder()
|
||||
.WithSmallTitle(Messages.InvalidTimeSpan, bot)
|
||||
.WithDescription(Messages.TimeSpanExample)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> AddReminderAsync(TimeSpan @in, string text, GuildData data,
|
||||
return await AddReminderAsync(timeSpan, text, data, channelId, executor, CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> AddReminderAsync(TimeSpan timeSpan, string text, GuildData data,
|
||||
Snowflake channelId, IUser executor, CancellationToken ct = default)
|
||||
{
|
||||
var memberData = data.GetOrCreateMemberData(executor.ID);
|
||||
var remindAt = DateTimeOffset.UtcNow.Add(@in);
|
||||
var remindAt = DateTimeOffset.UtcNow.Add(timeSpan);
|
||||
var responseResult = await _interactionApi.GetOriginalInteractionResponseAsync(_context.Interaction.ApplicationID, _context.Interaction.Token, ct);
|
||||
if (!responseResult.IsDefined(out var response))
|
||||
{
|
||||
|
@ -174,6 +194,133 @@ public class RemindCommandGroup : CommandGroup
|
|||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
public enum Parameters
|
||||
{
|
||||
[UsedImplicitly] Time,
|
||||
[UsedImplicitly] Text
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that edits a scheduled reminder using the specified text or time.
|
||||
/// </summary>
|
||||
/// <param name="position">The list position of the reminder to edit.</param>
|
||||
/// <param name="parameter">The reminder's parameter to edit.</param>
|
||||
/// <param name="value">The new value for the reminder as a text or time.</param>
|
||||
/// <returns>A feedback sending result which may or may not have succeeded.</returns>
|
||||
[Command("editremind")]
|
||||
[Description("Edit a reminder")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[RequireContext(ChannelContext.Guild)]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteEditReminderAsync(
|
||||
[Description("Position in list")] [MinValue(1)]
|
||||
int position,
|
||||
[Description("Parameter to edit")] Parameters parameter,
|
||||
[Description("Parameter's new value")] string value)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
return Result.FromError(executorResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
var memberData = data.GetOrCreateMemberData(executor.ID);
|
||||
|
||||
if (parameter is Parameters.Time)
|
||||
{
|
||||
return await EditReminderTimeAsync(position - 1, value, memberData, bot, executor, CancellationToken);
|
||||
}
|
||||
|
||||
return await EditReminderTextAsync(position - 1, value, memberData, bot, executor, CancellationToken);
|
||||
}
|
||||
|
||||
private async Task<Result> EditReminderTimeAsync(int index, string value, MemberData data,
|
||||
IUser bot, IUser executor, CancellationToken ct = default)
|
||||
{
|
||||
if (index >= data.Reminders.Count)
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.InvalidReminderPosition, bot)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var parseResult = TimeSpanParser.TryParse(value);
|
||||
if (!parseResult.IsDefined(out var timeSpan))
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder()
|
||||
.WithSmallTitle(Messages.InvalidTimeSpan, bot)
|
||||
.WithDescription(Messages.TimeSpanExample)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var oldReminder = data.Reminders[index];
|
||||
var remindAt = DateTimeOffset.UtcNow.Add(timeSpan);
|
||||
|
||||
data.Reminders.Add(oldReminder with { At = remindAt });
|
||||
data.Reminders.RemoveAt(index);
|
||||
|
||||
var builder = new StringBuilder()
|
||||
.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)
|
||||
.WithDescription(builder.ToString())
|
||||
.WithColour(ColorsList.Cyan)
|
||||
.WithFooter(string.Format(Messages.ReminderPosition, data.Reminders.Count))
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
private async Task<Result> EditReminderTextAsync(int index, string value, MemberData data,
|
||||
IUser bot, IUser executor, CancellationToken ct = default)
|
||||
{
|
||||
if (index >= data.Reminders.Count)
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.InvalidReminderPosition, bot)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct);
|
||||
}
|
||||
|
||||
var oldReminder = data.Reminders[index];
|
||||
|
||||
data.Reminders.Add(oldReminder with { Text = value });
|
||||
data.Reminders.RemoveAt(index);
|
||||
|
||||
var builder = new StringBuilder()
|
||||
.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)
|
||||
.WithDescription(builder.ToString())
|
||||
.WithColour(ColorsList.Cyan)
|
||||
.WithFooter(string.Format(Messages.ReminderPosition, data.Reminders.Count))
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that deletes a reminder using its list position.
|
||||
/// </summary>
|
||||
|
|
|
@ -47,6 +47,7 @@ public class SettingsCommandGroup : CommandGroup
|
|||
GuildSettings.RenameHoistedUsers,
|
||||
GuildSettings.PublicFeedbackChannel,
|
||||
GuildSettings.PrivateFeedbackChannel,
|
||||
GuildSettings.WelcomeMessagesChannel,
|
||||
GuildSettings.EventNotificationChannel,
|
||||
GuildSettings.DefaultRole,
|
||||
GuildSettings.MuteRole,
|
||||
|
@ -219,12 +220,8 @@ public class SettingsCommandGroup : CommandGroup
|
|||
var title = Messages.SettingSuccessfullyChanged;
|
||||
var description = builder.ToString();
|
||||
|
||||
var logResult = _utility.LogActionAsync(
|
||||
_utility.LogAction(
|
||||
data.Settings, channelId, executor, title, description, bot, ColorsList.Magenta, false, ct);
|
||||
if (!logResult.IsSuccess)
|
||||
{
|
||||
return Result.FromError(logResult.Error);
|
||||
}
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(title, bot)
|
||||
.WithDescription(description)
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Text;
|
|||
using JetBrains.Annotations;
|
||||
using Octobot.Data;
|
||||
using Octobot.Extensions;
|
||||
using Octobot.Parsers;
|
||||
using Octobot.Services;
|
||||
using Remora.Commands.Attributes;
|
||||
using Remora.Commands.Groups;
|
||||
|
@ -20,7 +21,7 @@ using Remora.Results;
|
|||
namespace Octobot.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Handles tool commands: /userinfo, /guildinfo, /random, /timestamp.
|
||||
/// Handles tool commands: /userinfo, /guildinfo, /random, /timestamp, /8ball.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class ToolsCommandGroup : CommandGroup
|
||||
|
@ -100,10 +101,10 @@ public class ToolsCommandGroup : CommandGroup
|
|||
{
|
||||
var builder = new StringBuilder().AppendLine($"### <@{target.ID}>");
|
||||
|
||||
if (target.GlobalName is not null)
|
||||
if (target.GlobalName.IsDefined(out var globalName))
|
||||
{
|
||||
builder.AppendBulletPointLine(Messages.UserInfoDisplayName)
|
||||
.AppendLine(Markdown.InlineCode(target.GlobalName));
|
||||
.AppendLine(Markdown.InlineCode(globalName));
|
||||
}
|
||||
|
||||
builder.AppendBulletPointLine(Messages.UserInfoDiscordUserSince)
|
||||
|
@ -418,7 +419,7 @@ public class ToolsCommandGroup : CommandGroup
|
|||
/// <summary>
|
||||
/// A slash command that shows the current timestamp with an optional offset in all styles supported by Discord.
|
||||
/// </summary>
|
||||
/// <param name="offset">The offset for the current timestamp.</param>
|
||||
/// <param name="stringOffset">The offset for the current timestamp.</param>
|
||||
/// <returns>
|
||||
/// A feedback sending result which may or may not have succeeded.
|
||||
/// </returns>
|
||||
|
@ -427,14 +428,20 @@ public class ToolsCommandGroup : CommandGroup
|
|||
[Description("Shows a timestamp in all styles")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteTimestampAsync(
|
||||
[Description("Offset from current time")]
|
||||
TimeSpan? offset = null)
|
||||
[Description("Offset from current time")] [Option("offset")]
|
||||
string? stringOffset = null)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
}
|
||||
|
||||
var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken);
|
||||
if (!executorResult.IsDefined(out var executor))
|
||||
{
|
||||
|
@ -444,6 +451,23 @@ public class ToolsCommandGroup : CommandGroup
|
|||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
if (stringOffset is null)
|
||||
{
|
||||
return await SendTimestampAsync(null, executor, CancellationToken);
|
||||
}
|
||||
|
||||
var parseResult = TimeSpanParser.TryParse(stringOffset);
|
||||
if (!parseResult.IsDefined(out var offset))
|
||||
{
|
||||
var failedEmbed = new EmbedBuilder()
|
||||
.WithSmallTitle(Messages.InvalidTimeSpan, bot)
|
||||
.WithDescription(Messages.TimeSpanExample)
|
||||
.WithColour(ColorsList.Red)
|
||||
.Build();
|
||||
|
||||
return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken);
|
||||
}
|
||||
|
||||
return await SendTimestampAsync(offset, executor, CancellationToken);
|
||||
}
|
||||
|
||||
|
@ -473,4 +497,65 @@ public class ToolsCommandGroup : CommandGroup
|
|||
|
||||
return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A slash command that shows a random answer from the Magic 8-Ball.
|
||||
/// </summary>
|
||||
/// <param name="question">Unused input.</param>
|
||||
/// <remarks>
|
||||
/// The 8-Ball answers were taken from <a href="https://en.wikipedia.org/wiki/Magic_8_Ball#Possible_answers">Wikipedia</a>.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// A feedback sending result which may or may not have succeeded.
|
||||
/// </returns>
|
||||
[Command("8ball")]
|
||||
[DiscordDefaultDMPermission(false)]
|
||||
[Description("Ask the Magic 8-Ball a question")]
|
||||
[UsedImplicitly]
|
||||
public async Task<Result> ExecuteEightBallAsync(
|
||||
// let the user think he's actually asking the ball a question
|
||||
string question)
|
||||
{
|
||||
if (!_context.TryGetContextIDs(out var guildId, out _, out _))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context");
|
||||
}
|
||||
|
||||
var botResult = await _userApi.GetCurrentUserAsync(CancellationToken);
|
||||
if (!botResult.IsDefined(out var bot))
|
||||
{
|
||||
return Result.FromError(botResult);
|
||||
}
|
||||
|
||||
var data = await _guildData.GetData(guildId, CancellationToken);
|
||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||
|
||||
return await AnswerEightBallAsync(bot, CancellationToken);
|
||||
}
|
||||
|
||||
private static readonly string[] AnswerTypes =
|
||||
[
|
||||
"Positive", "Questionable", "Neutral", "Negative"
|
||||
];
|
||||
|
||||
private Task<Result> AnswerEightBallAsync(IUser bot, CancellationToken ct)
|
||||
{
|
||||
var typeNumber = Random.Shared.Next(0, 4);
|
||||
var embedColor = typeNumber switch
|
||||
{
|
||||
0 => ColorsList.Blue,
|
||||
1 => ColorsList.Green,
|
||||
2 => ColorsList.Yellow,
|
||||
3 => ColorsList.Red,
|
||||
_ => throw new ArgumentOutOfRangeException(null, nameof(typeNumber))
|
||||
};
|
||||
|
||||
var answer = $"EightBall{AnswerTypes[typeNumber]}{Random.Shared.Next(1, 6)}".Localized();
|
||||
|
||||
var embed = new EmbedBuilder().WithSmallTitle(answer, bot)
|
||||
.WithColour(embedColor)
|
||||
.Build();
|
||||
|
||||
return _feedback.SendContextualEmbedResultAsync(embed, ct: ct);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,6 +68,11 @@ public static class GuildSettings
|
|||
/// </summary>
|
||||
public static readonly SnowflakeOption PrivateFeedbackChannel = new("PrivateFeedbackChannel");
|
||||
|
||||
/// <summary>
|
||||
/// Controls what channel should welcome messages be sent to.
|
||||
/// </summary>
|
||||
public static readonly SnowflakeOption WelcomeMessagesChannel = new("WelcomeMessagesChannel");
|
||||
|
||||
public static readonly SnowflakeOption EventNotificationChannel = new("EventNotificationChannel");
|
||||
public static readonly SnowflakeOption DefaultRole = new("DefaultRole");
|
||||
public static readonly SnowflakeOption MuteRole = new("MuteRole");
|
||||
|
|
|
@ -22,6 +22,7 @@ public enum AllOptionsEnum
|
|||
[UsedImplicitly] RenameHoistedUsers,
|
||||
[UsedImplicitly] PublicFeedbackChannel,
|
||||
[UsedImplicitly] PrivateFeedbackChannel,
|
||||
[UsedImplicitly] WelcomeMessagesChannel,
|
||||
[UsedImplicitly] EventNotificationChannel,
|
||||
[UsedImplicitly] DefaultRole,
|
||||
[UsedImplicitly] MuteRole,
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
using System.Text.Json.Nodes;
|
||||
using Remora.Commands.Parsers;
|
||||
using Octobot.Parsers;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Octobot.Data.Options;
|
||||
|
||||
public sealed class TimeSpanOption : Option<TimeSpan>
|
||||
{
|
||||
private static readonly TimeSpanParser Parser = new();
|
||||
|
||||
public TimeSpanOption(string name, TimeSpan defaultValue) : base(name, defaultValue) { }
|
||||
|
||||
public override TimeSpan Get(JsonNode settings)
|
||||
{
|
||||
var property = settings[Name];
|
||||
return property != null ? ParseTimeSpan(property.GetValue<string>()).Entity : DefaultValue;
|
||||
return property != null ? TimeSpanParser.TryParse(property.GetValue<string>()).Entity : DefaultValue;
|
||||
}
|
||||
|
||||
public override Result Set(JsonNode settings, string from)
|
||||
{
|
||||
if (!ParseTimeSpan(from).IsDefined(out var span))
|
||||
if (!TimeSpanParser.TryParse(from).IsDefined(out var span))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue);
|
||||
}
|
||||
|
@ -26,9 +24,4 @@ public sealed class TimeSpanOption : Option<TimeSpan>
|
|||
settings[Name] = span.ToString();
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
private static Result<TimeSpan> ParseTimeSpan(string from)
|
||||
{
|
||||
return Parser.TryParseAsync(from).AsTask().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ public static class LoggerExtensions
|
|||
return;
|
||||
}
|
||||
|
||||
logger.LogWarning("{UserMessage}\n{ResultErrorMessage}", message, result.Error.Message);
|
||||
logger.LogWarning("{UserMessage}{NewLine}{ResultErrorMessage}", message, Environment.NewLine,
|
||||
result.Error.Message);
|
||||
}
|
||||
}
|
||||
|
|
151
src/Messages.Designer.cs
generated
151
src/Messages.Designer.cs
generated
|
@ -1049,12 +1049,155 @@ namespace Octobot {
|
|||
}
|
||||
}
|
||||
|
||||
internal static string UserInfoKicked
|
||||
{
|
||||
get
|
||||
{
|
||||
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 SettingsWelcomeMessagesChannel
|
||||
{
|
||||
get {
|
||||
return ResourceManager.GetString("SettingsWelcomeMessagesChannel", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ using Microsoft.Extensions.Hosting;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Octobot.Commands.Events;
|
||||
using Octobot.Services;
|
||||
using Octobot.Services.Profiler;
|
||||
using Octobot.Services.Update;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Commands;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
|
@ -23,7 +22,7 @@ namespace Octobot;
|
|||
|
||||
public sealed class Octobot
|
||||
{
|
||||
public const string RepositoryUrl = "https://github.com/LabsDevelopment/Octobot";
|
||||
public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot";
|
||||
public const string IssuesUrl = $"{RepositoryUrl}/issues";
|
||||
|
||||
public static readonly AllowedMentions NoMentions = new(
|
||||
|
@ -87,8 +86,6 @@ public sealed class Octobot
|
|||
.AddPreparationErrorEvent<LoggingPreparationErrorEvent>()
|
||||
.AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
|
||||
// Services
|
||||
.AddTransient<Profiler>()
|
||||
.AddSingleton<ProfilerFactory>()
|
||||
.AddSingleton<Utility>()
|
||||
.AddSingleton<GuildDataService>()
|
||||
.AddHostedService<GuildDataService>(provider => provider.GetRequiredService<GuildDataService>())
|
||||
|
|
78
src/Parsers/TimeSpanParser.cs
Normal file
78
src/Parsers/TimeSpanParser.cs
Normal file
|
@ -0,0 +1,78 @@
|
|||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using JetBrains.Annotations;
|
||||
using Remora.Commands.Parsers;
|
||||
using Remora.Results;
|
||||
|
||||
namespace Octobot.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Parses <see cref="TimeSpan"/>s.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public partial class TimeSpanParser : AbstractTypeParser<TimeSpan>
|
||||
{
|
||||
private static readonly Regex Pattern = ParseRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="TimeSpan"/> from the <paramref name="timeSpanString"/>.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The parsed <see cref="TimeSpan"/>, or <see cref="ArgumentInvalidError"/> if parsing failed.
|
||||
/// </returns>
|
||||
public static Result<TimeSpan> TryParse(string timeSpanString)
|
||||
{
|
||||
if (timeSpanString.StartsWith('-'))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(timeSpanString), "TimeSpans cannot be negative.");
|
||||
}
|
||||
|
||||
if (TimeSpan.TryParse(timeSpanString, DateTimeFormatInfo.InvariantInfo, out var parsedTimeSpan))
|
||||
{
|
||||
return parsedTimeSpan;
|
||||
}
|
||||
|
||||
var matches = ParseRegex().Matches(timeSpanString);
|
||||
return matches.Count > 0
|
||||
? ParseFromRegex(matches)
|
||||
: new ArgumentInvalidError(nameof(timeSpanString), "The regex did not produce any matches.");
|
||||
}
|
||||
|
||||
private static Result<TimeSpan> ParseFromRegex(MatchCollection matches)
|
||||
{
|
||||
var timeSpan = TimeSpan.Zero;
|
||||
|
||||
foreach (var groups in matches.Select(match => match.Groups
|
||||
.Cast<Group>()
|
||||
.Where(g => g.Success)
|
||||
.Skip(1)
|
||||
.Select(g => (g.Name, g.Value))))
|
||||
{
|
||||
foreach ((var key, var groupValue) in groups)
|
||||
{
|
||||
if (!int.TryParse(groupValue, out var parsedIntegerValue))
|
||||
{
|
||||
return new ArgumentInvalidError(nameof(groupValue), "The input value was not an integer.");
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
timeSpan += key switch
|
||||
{
|
||||
"Years" => now.AddYears(parsedIntegerValue) - now,
|
||||
"Months" => now.AddMonths(parsedIntegerValue) - now,
|
||||
"Weeks" => TimeSpan.FromDays(parsedIntegerValue * 7),
|
||||
"Days" => TimeSpan.FromDays(parsedIntegerValue),
|
||||
"Hours" => TimeSpan.FromHours(parsedIntegerValue),
|
||||
"Minutes" => TimeSpan.FromMinutes(parsedIntegerValue),
|
||||
"Seconds" => TimeSpan.FromSeconds(parsedIntegerValue),
|
||||
_ => throw new ArgumentOutOfRangeException(key)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return timeSpan;
|
||||
}
|
||||
|
||||
[GeneratedRegex("(?<Years>\\d+(?=y|л|г))|(?<Months>\\d+(?=mo|мес))|(?<Weeks>\\d+(?=w|н|нед))|(?<Days>\\d+(?=d|д|дн))|(?<Hours>\\d+(?=h|ч))|(?<Minutes>\\d+(?=m|min|мин|м))|(?<Seconds>\\d+(?=s|sec|с|сек))")]
|
||||
private static partial Regex ParseRegex();
|
||||
}
|
|
@ -51,7 +51,7 @@ public class GuildMemberJoinedResponder : IResponder<IGuildMemberAdd>
|
|||
return Result.FromError(returnRolesResult.Error);
|
||||
}
|
||||
|
||||
if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty()
|
||||
if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty()
|
||||
|| GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled")
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
|
@ -76,7 +76,7 @@ public class GuildMemberJoinedResponder : IResponder<IGuildMemberAdd>
|
|||
.Build();
|
||||
|
||||
return await _channelApi.CreateMessageWithEmbedResultAsync(
|
||||
GuildSettings.PublicFeedbackChannel.Get(cfg), embedResult: embed,
|
||||
GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed,
|
||||
allowedMentions: Octobot.NoMentions, ct: ct);
|
||||
}
|
||||
|
||||
|
|
|
@ -28,9 +28,10 @@ public class MessageCreateResponder : IResponder<IMessageCreate>
|
|||
"whoami" => "`nobody`",
|
||||
"сука !!" => "`root`",
|
||||
"воооо" => "`removing /...`",
|
||||
"пон" => "https://cdn.upload.systems/uploads/2LNfUSwM.jpg",
|
||||
"пон" => "https://i.ibb.co/Kw6QVcw/parry.jpg",
|
||||
"++++" => "#",
|
||||
"осу" => "https://github.com/ppy/osu",
|
||||
"лан" => "https://i.ibb.co/VYH2QLc/lan.jpg",
|
||||
_ => default(Optional<string>)
|
||||
});
|
||||
return Task.FromResult(Result.FromSuccess());
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remora.Results;
|
||||
|
||||
// TODO: remove in future profiler PRs
|
||||
// ReSharper disable All
|
||||
|
||||
namespace Octobot.Services.Profiler;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the ability to profile how long certain parts of code take to complete using <see cref="Stopwatch"/>es.
|
||||
/// </summary>
|
||||
/// <remarks>Resolve <see cref="ProfilerFactory"/> instead in singletons.</remarks>
|
||||
public sealed class Profiler
|
||||
{
|
||||
private const int MaxProfilerTime = 1000; // milliseconds
|
||||
private readonly List<ProfilerEvent> _events = [];
|
||||
private readonly ILogger<Profiler> _logger;
|
||||
|
||||
public Profiler(ILogger<Profiler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes an event to the profiler.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the event.</param>
|
||||
public void Push(string id)
|
||||
{
|
||||
_events.Add(new ProfilerEvent
|
||||
{
|
||||
Id = id,
|
||||
Stopwatch = Stopwatch.StartNew()
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pops the last pushed event from the profiler.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the profiler contains no events.</exception>
|
||||
public void Pop()
|
||||
{
|
||||
if (_events.Count is 0)
|
||||
{
|
||||
throw new InvalidOperationException("Nothing to pop");
|
||||
}
|
||||
|
||||
_events.Last().Stopwatch.Stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the profiler took too long to execute, this will log a warning with per-event time usage
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
private void Report()
|
||||
{
|
||||
var main = _events[0];
|
||||
if (main.Stopwatch.ElapsedMilliseconds < MaxProfilerTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var unprofiled = main.Stopwatch.ElapsedMilliseconds;
|
||||
var builder = new StringBuilder().AppendLine();
|
||||
for (var i = 1; i < _events.Count; i++)
|
||||
{
|
||||
var profilerEvent = _events[i];
|
||||
if (profilerEvent.Stopwatch.IsRunning)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Tried to report on a profiler with running stopwatches: {profilerEvent.Id}");
|
||||
}
|
||||
|
||||
builder.AppendLine($"{profilerEvent.Id}: {profilerEvent.Stopwatch.ElapsedMilliseconds}ms");
|
||||
unprofiled -= profilerEvent.Stopwatch.ElapsedMilliseconds;
|
||||
}
|
||||
|
||||
builder.AppendLine($"<unprofiled>: {unprofiled}ms");
|
||||
|
||||
_logger.LogWarning("Profiler {ID} took {Elapsed} milliseconds to execute (max: {Max}ms):{Events}", main.Id,
|
||||
main.Stopwatch.ElapsedMilliseconds, MaxProfilerTime, builder.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Pop"/> the profiler and <see cref="Report"/> on it afterwards.
|
||||
/// </summary>
|
||||
public void PopAndReport()
|
||||
{
|
||||
Pop();
|
||||
Report();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="PopAndReport"/> on the profiler and return a <see cref="Result{TEntity}"/>.
|
||||
/// </summary>
|
||||
/// <param name="result"></param>
|
||||
/// <returns></returns>
|
||||
public Result ReportWithResult(Result result)
|
||||
{
|
||||
PopAndReport();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="ReportWithResult"/> with <see cref="Result.FromSuccess"/>
|
||||
/// </summary>
|
||||
/// <returns>A successful result.</returns>
|
||||
public Result ReportWithSuccess()
|
||||
{
|
||||
return ReportWithResult(Result.FromSuccess());
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
using System.Diagnostics;
|
||||
|
||||
namespace Octobot.Services.Profiler;
|
||||
|
||||
public struct ProfilerEvent
|
||||
{
|
||||
public string Id { get; init; }
|
||||
public Stopwatch Stopwatch { get; init; }
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Octobot.Services.Profiler;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a method to create a <see cref="Profiler"/>. Useful in singletons.
|
||||
/// </summary>
|
||||
public sealed class ProfilerFactory
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
public ProfilerFactory(IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Profiler"/>.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="Profiler"/>.</returns>
|
||||
// TODO: remove in future profiler PRs
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
public Profiler Create()
|
||||
{
|
||||
return _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<Profiler>();
|
||||
}
|
||||
}
|
|
@ -185,7 +185,7 @@ public sealed partial class MemberUpdateService : BackgroundService
|
|||
{
|
||||
var currentNickname = member.Nickname.IsDefined(out var nickname)
|
||||
? nickname
|
||||
: user.GlobalName ?? user.Username;
|
||||
: user.GlobalName.OrDefault(user.Username);
|
||||
var characterList = currentNickname.ToList();
|
||||
var usernameChanged = false;
|
||||
foreach (var character in currentNickname)
|
||||
|
|
|
@ -196,7 +196,7 @@ public sealed class Utility
|
|||
/// </param>
|
||||
/// <param name="ct">The cancellation token for this operation.</param>
|
||||
/// <returns>A result which has succeeded.</returns>
|
||||
public Result LogActionAsync(
|
||||
public void LogAction(
|
||||
JsonNode cfg, Snowflake channelId, IUser user, string title, string description, IUser avatar,
|
||||
Color color, bool isPublic = true, CancellationToken ct = default)
|
||||
{
|
||||
|
@ -205,7 +205,7 @@ public sealed class Utility
|
|||
if (GuildSettings.PublicFeedbackChannel.Get(cfg).EmptyOrEqualTo(channelId)
|
||||
&& GuildSettings.PrivateFeedbackChannel.Get(cfg).EmptyOrEqualTo(channelId))
|
||||
{
|
||||
return Result.FromSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
var logEmbed = new EmbedBuilder().WithSmallTitle(title, avatar)
|
||||
|
@ -230,8 +230,6 @@ public sealed class Utility
|
|||
privateChannel, embedResult: logEmbed,
|
||||
ct: ct);
|
||||
}
|
||||
|
||||
return Result.FromSuccess();
|
||||
}
|
||||
|
||||
public async Task<Result<Snowflake>> GetEmergencyFeedbackChannel(IGuild guild, GuildData data, CancellationToken ct)
|
||||
|
|
Loading…
Add table
Reference in a new issue