1
0
Fork 1
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:
Macintxsh 2024-03-18 15:05:24 +03:00 committed by GitHub
commit 196f65c1a4
Signed by: GitHub
GPG key ID: B5690EEEBB952194
32 changed files with 774 additions and 259 deletions

4
.github/CODEOWNERS vendored
View file

@ -1,2 +1,2 @@
* @LabsDevelopment/octobot
/docs/ @LabsDevelopment/octobot-docs
* @TeamOctolings/octobot
/docs/ @TeamOctolings/octobot-docs

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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);
}
return await AddReminderAsync(timeSpan, text, data, channelId, executor, CancellationToken);
}
private async Task<Result> AddReminderAsync(TimeSpan @in, string text, GuildData data,
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>

View file

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

View file

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

View file

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

View file

@ -22,6 +22,7 @@ public enum AllOptionsEnum
[UsedImplicitly] RenameHoistedUsers,
[UsedImplicitly] PublicFeedbackChannel,
[UsedImplicitly] PrivateFeedbackChannel,
[UsedImplicitly] WelcomeMessagesChannel,
[UsedImplicitly] EventNotificationChannel,
[UsedImplicitly] DefaultRole,
[UsedImplicitly] MuteRole,

View file

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

View file

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

153
src/Messages.Designer.cs generated
View file

@ -1046,15 +1046,158 @@ namespace Octobot {
internal static string SettingsLeaveMessage {
get {
return ResourceManager.GetString("SettingsLeaveMessage", resourceCulture);
}
}
}
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);
}
}
}
}

View file

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

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

View file

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

View file

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

View file

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

View file

@ -1,9 +0,0 @@
using System.Diagnostics;
namespace Octobot.Services.Profiler;
public struct ProfilerEvent
{
public string Id { get; init; }
public Stopwatch Stopwatch { get; init; }
}

View file

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

View file

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

View file

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