mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-04-19 16:33:36 +03:00
Merge branch 'master' into profile-editremind
Signed-off-by: Macintxsh <95250141+mctaylors@users.noreply.github.com>
This commit is contained in:
commit
bdef465a41
15 changed files with 164 additions and 35 deletions
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
|
@ -1,2 +1,2 @@
|
||||||
* @LabsDevelopment/octobot
|
* @TeamOctolings/octobot
|
||||||
/docs/ @LabsDevelopment/octobot-docs
|
/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
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: ReSharper CLI InspectCode
|
- name: ReSharper CLI InspectCode
|
||||||
uses: muno92/resharper_inspectcode@1.11.1
|
uses: muno92/resharper_inspectcode@1.11.5
|
||||||
with:
|
with:
|
||||||
solutionPath: ./Octobot.sln
|
solutionPath: ./Octobot.sln
|
||||||
ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor
|
ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor
|
||||||
|
|
|
@ -9,11 +9,11 @@
|
||||||
<Title>Octobot</Title>
|
<Title>Octobot</Title>
|
||||||
<Authors>Octol1ttle, mctaylors, neroduckale</Authors>
|
<Authors>Octol1ttle, mctaylors, neroduckale</Authors>
|
||||||
<Copyright>AGPLv3</Copyright>
|
<Copyright>AGPLv3</Copyright>
|
||||||
<PackageProjectUrl>https://github.com/LabsDevelopment/Octobot</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/TeamOctolings/Octobot</PackageProjectUrl>
|
||||||
<PackageLicenseUrl>https://github.com/LabsDevelopment/Octobot/blob/master/LICENSE</PackageLicenseUrl>
|
<PackageLicenseUrl>https://github.com/TeamOctolings/Octobot/blob/master/LICENSE</PackageLicenseUrl>
|
||||||
<RepositoryUrl>https://github.com/LabsDevelopment/Octobot</RepositoryUrl>
|
<RepositoryUrl>https://github.com/TeamOctolings/Octobot</RepositoryUrl>
|
||||||
<RepositoryType>github</RepositoryType>
|
<RepositoryType>github</RepositoryType>
|
||||||
<Company>LabsDevelopment</Company>
|
<Company>TeamOctolings</Company>
|
||||||
<NeutralLanguage>en</NeutralLanguage>
|
<NeutralLanguage>en</NeutralLanguage>
|
||||||
<Description>A general-purpose Discord bot for moderation written in C#</Description>
|
<Description>A general-purpose Discord bot for moderation written in C#</Description>
|
||||||
<ApplicationIcon>docs/octobot.ico</ApplicationIcon>
|
<ApplicationIcon>docs/octobot.ico</ApplicationIcon>
|
||||||
|
|
|
@ -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
|
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.
|
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
|
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
|
are no PRs associated with it
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://cdn.mctaylors.ru/octobot-banner.png" alt="Octobot banner"/>
|
<img src="octobot-banner.png" alt="Octobot banner"/>
|
||||||
</p>
|
</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/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
|
## Features
|
||||||
|
|
||||||
|
@ -19,25 +19,13 @@ Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) wr
|
||||||
|
|
||||||
*...a-a-and more!*
|
*...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
|
## Building Octobot
|
||||||
|
|
||||||
1. Install [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
|
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!
|
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.
|
3. Clone this repository and open `Octobot` folder.
|
||||||
```
|
```
|
||||||
git clone https://github.com/LabsDevelopment/Octobot
|
git clone https://github.com/TeamOctolings/Octobot
|
||||||
cd Octobot
|
cd Octobot
|
||||||
```
|
```
|
||||||
4. Run Octobot using `dotnet` with `BOT_TOKEN` variable.
|
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,4 +591,7 @@
|
||||||
<data name="UserInfoKicked" xml:space="preserve">
|
<data name="UserInfoKicked" xml:space="preserve">
|
||||||
<value>Kicked</value>
|
<value>Kicked</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ReminderEdited" xml:space="preserve">
|
||||||
|
<value>Reminder edited</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
@ -591,4 +591,7 @@
|
||||||
<data name="UserInfoKicked" xml:space="preserve">
|
<data name="UserInfoKicked" xml:space="preserve">
|
||||||
<value>Выгнан</value>
|
<value>Выгнан</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ReminderEdited" xml:space="preserve">
|
||||||
|
<value>Напоминание отредактировано</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
@ -591,4 +591,7 @@
|
||||||
<data name="UserInfoKicked" xml:space="preserve">
|
<data name="UserInfoKicked" xml:space="preserve">
|
||||||
<value>кикнут</value>
|
<value>кикнут</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ReminderEdited" xml:space="preserve">
|
||||||
|
<value>напоминалка подправлена</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
|
@ -115,7 +115,7 @@ public class AboutCommandGroup : CommandGroup
|
||||||
.WithSmallTitle(string.Format(Messages.AboutBot, bot.Username), bot)
|
.WithSmallTitle(string.Format(Messages.AboutBot, bot.Username), bot)
|
||||||
.WithDescription(builder.ToString())
|
.WithDescription(builder.ToString())
|
||||||
.WithColour(ColorsList.Cyan)
|
.WithColour(ColorsList.Cyan)
|
||||||
.WithImageUrl("https://cdn.mctaylors.ru/octobot-banner.png")
|
.WithImageUrl("https://i.ibb.co/fS6wZhh/octobot-banner.png")
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var repositoryButton = new ButtonComponent(
|
var repositoryButton = new ButtonComponent(
|
||||||
|
|
|
@ -57,7 +57,7 @@ public class BanCommandGroup : CommandGroup
|
||||||
/// A slash command that bans a Discord user with the specified reason.
|
/// A slash command that bans a Discord user with the specified reason.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="target">The user to ban.</param>
|
/// <param name="target">The user to ban.</param>
|
||||||
/// <param name="stringDuration">The duration for this ban. The user will be automatically unbanned after this duration.</param>
|
/// <param name="duration">The duration for this ban. The user will be automatically unbanned after this duration.</param>
|
||||||
/// <param name="reason">
|
/// <param name="reason">
|
||||||
/// The reason for this ban. Must be encoded with <see cref="StringExtensions.EncodeHeader" /> when passed to
|
/// The reason for this ban. Must be encoded with <see cref="StringExtensions.EncodeHeader" /> when passed to
|
||||||
/// <see cref="IDiscordRestGuildAPI.CreateGuildBanAsync" />.
|
/// <see cref="IDiscordRestGuildAPI.CreateGuildBanAsync" />.
|
||||||
|
@ -79,8 +79,7 @@ public class BanCommandGroup : CommandGroup
|
||||||
[Description("User to ban")] IUser target,
|
[Description("User to ban")] IUser target,
|
||||||
[Description("Ban reason")] [MaxLength(256)]
|
[Description("Ban reason")] [MaxLength(256)]
|
||||||
string reason,
|
string reason,
|
||||||
[Description("Ban duration")] [Option("duration")]
|
[Description("Ban duration")] string? duration = null)
|
||||||
string? stringDuration = null)
|
|
||||||
{
|
{
|
||||||
_profiler.Push("ban_command");
|
_profiler.Push("ban_command");
|
||||||
_profiler.Push("preparation");
|
_profiler.Push("preparation");
|
||||||
|
@ -120,7 +119,7 @@ public class BanCommandGroup : CommandGroup
|
||||||
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
Messages.Culture = GuildSettings.Language.Get(data.Settings);
|
||||||
_profiler.Pop();
|
_profiler.Pop();
|
||||||
|
|
||||||
if (stringDuration is null)
|
if (duration is null)
|
||||||
{
|
{
|
||||||
_profiler.Pop();
|
_profiler.Pop();
|
||||||
return _profiler.ReportWithResult(await BanUserAsync(executor, target, reason, null, guild, data, channelId,
|
return _profiler.ReportWithResult(await BanUserAsync(executor, target, reason, null, guild, data, channelId,
|
||||||
|
@ -128,8 +127,8 @@ public class BanCommandGroup : CommandGroup
|
||||||
CancellationToken));
|
CancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
var parseResult = TimeSpanParser.TryParse(stringDuration);
|
var parseResult = TimeSpanParser.TryParse(duration);
|
||||||
if (!parseResult.IsDefined(out var duration))
|
if (!parseResult.IsDefined(out var timeSpan))
|
||||||
{
|
{
|
||||||
_profiler.Push("invalid_timespan_send");
|
_profiler.Push("invalid_timespan_send");
|
||||||
var failedEmbed = new EmbedBuilder()
|
var failedEmbed = new EmbedBuilder()
|
||||||
|
@ -142,7 +141,7 @@ public class BanCommandGroup : CommandGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
_profiler.Pop();
|
_profiler.Pop();
|
||||||
return _profiler.ReportWithResult(await BanUserAsync(executor, target, reason, duration, guild, data, channelId,
|
return _profiler.ReportWithResult(await BanUserAsync(executor, target, reason, timeSpan, guild, data, channelId,
|
||||||
bot, CancellationToken));
|
bot, CancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -236,6 +236,132 @@ public class RemindCommandGroup : CommandGroup
|
||||||
return _profiler.PopWithResult(await _feedback.SendContextualEmbedResultAsync(embed, ct: ct));
|
return _profiler.PopWithResult(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)
|
||||||
|
.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>
|
/// <summary>
|
||||||
/// A slash command that deletes a reminder using its list position.
|
/// A slash command that deletes a reminder using its list position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
6
src/Messages.Designer.cs
generated
6
src/Messages.Designer.cs
generated
|
@ -1052,5 +1052,11 @@ namespace Octobot {
|
||||||
return ResourceManager.GetString("UserInfoKicked", resourceCulture);
|
return ResourceManager.GetString("UserInfoKicked", resourceCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static string ReminderEdited {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ReminderEdited", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ namespace Octobot;
|
||||||
|
|
||||||
public sealed class 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 const string IssuesUrl = $"{RepositoryUrl}/issues";
|
||||||
|
|
||||||
public static readonly AllowedMentions NoMentions = new(
|
public static readonly AllowedMentions NoMentions = new(
|
||||||
|
|
|
@ -28,9 +28,10 @@ public class MessageCreateResponder : IResponder<IMessageCreate>
|
||||||
"whoami" => "`nobody`",
|
"whoami" => "`nobody`",
|
||||||
"сука !!" => "`root`",
|
"сука !!" => "`root`",
|
||||||
"воооо" => "`removing /...`",
|
"воооо" => "`removing /...`",
|
||||||
"пон" => "https://cdn.upload.systems/uploads/2LNfUSwM.jpg",
|
"пон" => "https://i.ibb.co/Kw6QVcw/parry.jpg",
|
||||||
"++++" => "#",
|
"++++" => "#",
|
||||||
"осу" => "https://github.com/ppy/osu",
|
"осу" => "https://github.com/ppy/osu",
|
||||||
|
"лан" => "https://i.ibb.co/VYH2QLc/lan.jpg",
|
||||||
_ => default(Optional<string>)
|
_ => default(Optional<string>)
|
||||||
});
|
});
|
||||||
return Task.FromResult(Result.FromSuccess());
|
return Task.FromResult(Result.FromSuccess());
|
||||||
|
|
Loading…
Add table
Reference in a new issue