mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 17:28:27 -04:00
Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2c3ada4710 | ||
|
0df3c1a4a1 | ||
|
ac589e0461 | ||
|
8f181eed85 | ||
|
6fefce4c4d | ||
|
d9e080f4b9 | ||
|
762a2eca1f | ||
|
2fba771681 | ||
|
b5e2b6f483 | ||
|
17e5ff8b89 | ||
|
3d287b2afa | ||
|
3f33274cec | ||
|
ee9d8a51bf | ||
|
80a7678a82 | ||
|
de8a0e2207 | ||
|
122b3ae0d9 | ||
|
b4307f9123 | ||
|
5ae18ba1bf | ||
|
44c8c9f459 | ||
|
4e177ff198 | ||
|
ad679a996d | ||
|
7d86a5e3eb | ||
|
214c9a383c | ||
|
f77e1c6b8c | ||
|
7e784b9507 | ||
|
7a14991ed6 | ||
|
4c5c2d7f6e | ||
|
87b90b47ce | ||
|
9f060243f0 | ||
|
d3ab32a7ac | ||
|
7bd081b7cf | ||
|
3a5b482884 | ||
|
db66264bc6 | ||
|
ae1ddd82d0 | ||
|
8523abd6f1 | ||
|
e1892c4ff4 | ||
|
a50a7b3b0e | ||
|
9d2268a925 | ||
|
d77a86c08b | ||
|
d605f685cf | ||
|
bbc1fd28c2 | ||
|
cff8a258d0 | ||
|
1d760a548e | ||
|
25fa8a3852 | ||
|
ca13684c0d | ||
|
0ad6b741e7 | ||
|
4ce756d760 | ||
|
5f2813d3af | ||
|
1b7458529c | ||
|
9c9c8d7490 | ||
|
2700bfdce8 | ||
|
5498c5ce3f |
48
CHANGELOG.md
48
CHANGELOG.md
@@ -2,6 +2,54 @@
|
||||
|
||||
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
||||
|
||||
### Added
|
||||
|
||||
- Added: Added a `.afk <msg>?` command which sets an afk message which will trigger whenever someone pings you
|
||||
- Message will when you type a message in any channel that the bot sees, or after 8 hours, whichever comes first
|
||||
- The specified message will be prefixed with "The user is afk: "
|
||||
- The afk message will disappear 30 seconds after being triggered
|
||||
|
||||
## [5.1.4] - 13.07.2024
|
||||
|
||||
### Added
|
||||
|
||||
- Added `.coins` command which lists top 10 cryptos ordered by marketcap
|
||||
- Added Clubs rank in the leaderboard to `.clubinfo`
|
||||
- Bot owners can now check other people's bank balance (Not server owners, only bot owner, the person who is hosting the bot)
|
||||
- You can now send multiple waifu gifts at once to waifus. For example `.waifugift 3xRose @user` will give that user 3 roses
|
||||
- The format is `<NUMBER>x<ITEM>`, no spaces
|
||||
- Added `.boosttest` command
|
||||
- Added support for any openai compatible api for the chatterbot feature change:
|
||||
- Changed games.yml to allow input of the apiUrl (needs to be openai compatible) and modelName as a string.
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated command strings to clarify `.say` and `.send` usages
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `.waifugift` help string
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed selfhost button from `.donate` command, no idea why it was there in the first place
|
||||
|
||||
## [5.1.3] - 06.07.2024
|
||||
|
||||
### Added
|
||||
|
||||
- Added `.quran` command, which will show the provided ayah in english and arabic, including recitation by Alafasy
|
||||
|
||||
### Changed
|
||||
|
||||
- Replying to the bot's message in the channel where chatterbot is enabled will also trigger the ai response, as if you pinged the bot. This only works for chatterbot, but not for nadeko ai command prompts
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `.stickeradd` it now properly supports 300x300 image uploads.
|
||||
- Bot should now trim the invalid characters from chatterbot usernames to avoid openai errors
|
||||
- Fixed prompt triggering chatterbot responses twice
|
||||
|
||||
## [5.1.2] - 29.06.2024
|
||||
|
||||
### Fixed
|
||||
|
@@ -12,7 +12,6 @@ ProjectSection(SolutionItems) = preProject
|
||||
README.md = README.md
|
||||
.gitlab-ci.yml = .gitlab-ci.yml
|
||||
Dockerfile = Dockerfile
|
||||
NuGet.Config = NuGet.Config
|
||||
migrate.ps1 = migrate.ps1
|
||||
remove-migration.ps1 = remove-migration.ps1
|
||||
EndProjectSection
|
||||
|
@@ -1,6 +0,0 @@
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
<add key="nadeko.bot" value="https://www.myget.org/F/nadeko/api/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
</configuration>
|
@@ -1,8 +1,55 @@
|
||||
# Creating A Medusa
|
||||
|
||||
## Theory
|
||||
## Getting started
|
||||
|
||||
### Introduction
|
||||
This section will guide you through how to create a simple custom medusa. You can find the entirety of this code hosted [here](https://gitlab.com/nadeko/example_medusa)
|
||||
|
||||
#### Prerequisite
|
||||
- [.net8 sdk](https://dotnet.microsoft.com/en-us/download) installed
|
||||
- Optional: use [vscode](https://code.visualstudio.com/download) to write code
|
||||
|
||||
#### Guide
|
||||
|
||||
- Open your favorite terminal and navigate to a folder where you will keep your project .
|
||||
|
||||
- Create a new folder and move into it
|
||||
- `mkdir example_medusa `
|
||||
- `cd example_medusa`
|
||||
|
||||
- Install nadeko-medusa template
|
||||
- `dotnet new install nadeko-medusa`
|
||||
|
||||
- Make a new Nadeko Medusa project
|
||||
- `dotnet new nadeko-medusa`
|
||||
|
||||
### Build it
|
||||
|
||||
- Build your Medusa into a dll that Nadeko can load. In your terminal, type:
|
||||
- `dotnet publish -o bin/medusae/example_medusa /p:DebugType=embedded`
|
||||
|
||||
- Done. You can now try it out in action.
|
||||
|
||||
### Try it out
|
||||
|
||||
- Copy the `bin/medusae/example_medusa` folder into your NadekoBot's `data/medusae/` folder. (Nadeko version 4.1.0+)
|
||||
|
||||
- Load it with `.meload example_medusa`
|
||||
|
||||
- In the channel your bot can see, run the following commands to try it out
|
||||
- `.hello` and
|
||||
- `.hello @<someone>`
|
||||
|
||||
- Check its information with
|
||||
- `.meinfo example_medusa`
|
||||
|
||||
- Unload it
|
||||
- `.meunload example_medusa`
|
||||
|
||||
- :tada: Congrats! You've just made your first medusa! :tada:
|
||||
|
||||
|
||||
|
||||
## Theory
|
||||
|
||||
Medusa system allows you to write independent medusae (known as "modules", "cogs" or "plugins" in other software) which you can then load, unload and update at will without restarting the bot.
|
||||
|
||||
@@ -99,9 +146,9 @@ If you don't want any auxiliary files, and you don't want to bother making new .
|
||||
|
||||
If you update your response strings .yml file(s) while the medusa is loaded and running, running `.stringsreload` will reload the responses without the need to reload the medusa or restart the bot.
|
||||
|
||||
#### Config
|
||||
#### Bot medusa config file
|
||||
|
||||
- Medusa config is kept in `medusae/medusa.yml` file
|
||||
- Medusa config is kept in `data/medusae/medusa.yml` file in NadekoBot installation folder
|
||||
- At the moment this config only keeps track of which medusae are currently loaded (they will also be always loaded at startup)
|
||||
- If a medusa is causing issues and you're unable to unload it, you can remove it from the `loaded:` list in this config file and restart the bot. It won't be loaded next time the bot is started up
|
||||
|
||||
@@ -115,138 +162,4 @@ To make sure your medusa can be properly unloaded/reloaded you must:
|
||||
|
||||
- If you are still having issues, you can always run `.meunload` followed by a bot restart, or if you want to find what is causing the medusa unloadability issues, you can check the [microsoft's assembly unloadability debugging guide](https://docs.microsoft.com/en-us/dotnet/standard/assembly/unloadability)
|
||||
|
||||
## Practice
|
||||
|
||||
This section will guide you through how to create a simple custom medusa. You can find the entirety of this code hosted [here](https://gitlab.com/nadeko/example_medusa)
|
||||
|
||||
#### Prerequisite
|
||||
- [.net6 sdk](https://dotnet.microsoft.com/en-us/download) installed
|
||||
- Optional: use [vscode](https://code.visualstudio.com/download) to write code
|
||||
|
||||
#### Guide
|
||||
|
||||
|
||||
- Open your favorite terminal and navigate to a folder where you will keep your project .
|
||||
|
||||
- Create a new folder
|
||||
- `mkdir example_medusa`
|
||||
- Create a new .net class library
|
||||
- `dotnet new classlib`
|
||||
- Open the current folder with your favorite editor/IDE. In this case we'll use VsCode
|
||||
- `code .`
|
||||
- Remove the `Class1.cs` file
|
||||
- Replace the contents of the `.csproj` file with the following contents
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
|
||||
<!-- Reduces some boilerplate in your .cs files -->
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
<!-- Use latest .net features -->
|
||||
<LangVersion>preview</LangVersion>
|
||||
<EnablePreviewFeatures>true</EnablePreviewFeatures>
|
||||
<GenerateRequiresPreviewFeaturesAttribute>true</GenerateRequiresPreviewFeaturesAttribute>
|
||||
|
||||
<!-- tell .net that this library will be used as a plugin -->
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Base medusa package. You MUST reference this in order to have a working medusa -->
|
||||
<!-- Also, this package comes from MyGet, which requires you to have a NuGet.Config file next to your .csproj -->
|
||||
<PackageReference Include="Nadeko.Medusa" Version="4.3.9">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
<!-- Note: If you want to use NadekoBot services etc... You will have to manually clone
|
||||
the https://gitlab.com/kwoth/nadekobot repo locally and reference the NadekoBot.csproj because there is no NadekoBot package atm.
|
||||
It is strongly recommended that you checkout a specific tag which matches your version of nadeko,
|
||||
as there could be breaking changes even between minor versions of NadekoBot.
|
||||
For example if you're running NadekoBot 4.1.0 locally for which you want to create a medusa for,
|
||||
you should do "git checkout 4.1.0" in your NadekoBot solution and then reference the NadekoBot.csproj
|
||||
-->
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Copy shortcut and full strings to output (if they exist) -->
|
||||
<ItemGroup>
|
||||
<None Update="res.yml;cmds.yml;strings/**">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
```
|
||||
- Create a `MySnek.cs` file and add the following contents
|
||||
```cs
|
||||
using Nadeko.Snake;
|
||||
using NadekoBot;
|
||||
using Discord;
|
||||
|
||||
public sealed class MySnek : Snek
|
||||
{
|
||||
[cmd]
|
||||
public async Task Hello(AnyContext ctx)
|
||||
{
|
||||
await ctx.Channel.SendMessageAsync($"Hello everyone!");
|
||||
}
|
||||
|
||||
[cmd]
|
||||
public async Task Hello(AnyContext ctx, IUser target)
|
||||
{
|
||||
await ctx.ConfirmLocalizedAsync("hello", target);
|
||||
}
|
||||
}
|
||||
```
|
||||
- Create `res.yml` and `cmds.yml` files with the following contents
|
||||
`res.yml`
|
||||
```yml
|
||||
medusa.description: "This is my medusa's description"
|
||||
hello: "Hello {0}, from res.yml!"
|
||||
```
|
||||
|
||||
`cmds.yml`
|
||||
```yml
|
||||
hello:
|
||||
desc: "This is a basic hello command"
|
||||
args:
|
||||
- ""
|
||||
- "@Someone"
|
||||
```
|
||||
|
||||
- Add `NuGet.Config` file which will let you use the base Nadeko.Medusa package. This file should always look like this and you shouldn't change it
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
<add key="nadeko.bot" value="https://www.myget.org/F/nadeko/api/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
### Build it
|
||||
|
||||
- Build your Medusa into a dll that Nadeko can load. In your terminal, type:
|
||||
- `dotnet publish -o bin/medusae/example_medusa /p:DebugType=embedded`
|
||||
|
||||
- Done. You can now try it out in action.
|
||||
|
||||
### Try it out
|
||||
|
||||
- Copy the `bin/medusae/example_medusa` folder into your NadekoBot's `data/medusae/` folder. (Nadeko version 4.1.0+)
|
||||
|
||||
- Load it with `.meload example_medusa`
|
||||
|
||||
- In the channel your bot can see, run the following commands to try it out
|
||||
- `.hello` and
|
||||
- `.hello @<someone>`
|
||||
|
||||
- Check its information with
|
||||
- `.meinfo example_medusa`
|
||||
|
||||
- Unload it
|
||||
- `.meunload example_medusa`
|
||||
|
||||
- Congrats! You've just made your first medusa!
|
||||
|
@@ -9,7 +9,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Discord.Net.Core" Version="3.204.0" />
|
||||
<PackageReference Include="Discord.Net.Core" Version="3.15.3" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="15.1.4" />
|
||||
</ItemGroup>
|
||||
|
@@ -77,7 +77,6 @@ csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
|
||||
# Expression-bodied members
|
||||
csharp_style_expression_bodied_accessors = true:suggestion
|
||||
csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_indexers = true:suggestion
|
||||
csharp_style_expression_bodied_lambdas = true:suggestion
|
||||
csharp_style_expression_bodied_local_functions = true:suggestion
|
||||
@@ -181,9 +180,9 @@ dotnet_naming_rule.private_readonly_field.symbols = private_readonly_field
|
||||
dotnet_naming_rule.private_readonly_field.style = begins_with_underscore
|
||||
dotnet_naming_rule.private_readonly_field.severity = warning
|
||||
|
||||
dotnet_naming_rule.private_field.symbols = private_field
|
||||
dotnet_naming_rule.private_field.style = camel_case
|
||||
dotnet_naming_rule.private_field.severity = warning
|
||||
# dotnet_naming_rule.private_field.symbols = private_field
|
||||
# dotnet_naming_rule.private_field.style = camel_case
|
||||
# dotnet_naming_rule.private_field.severity = warning
|
||||
|
||||
dotnet_naming_rule.const_fields.symbols = const_fields
|
||||
dotnet_naming_rule.const_fields.style = all_upper
|
||||
|
@@ -182,15 +182,6 @@ public static class GuildConfigExtensions
|
||||
.SelectMany(gc => gc.FollowedStreams)
|
||||
.ToList();
|
||||
|
||||
public static void SetCleverbotEnabled(this DbSet<GuildConfig> configs, ulong id, bool cleverbotEnabled)
|
||||
{
|
||||
var conf = configs.FirstOrDefault(gc => gc.GuildId == id);
|
||||
|
||||
if (conf is null)
|
||||
return;
|
||||
|
||||
conf.CleverbotEnabled = cleverbotEnabled;
|
||||
}
|
||||
|
||||
public static XpSettings XpSettingsFor(this DbContext ctx, ulong guildId)
|
||||
{
|
||||
|
@@ -27,5 +27,15 @@ public partial class Administration
|
||||
.Confirm($"{result.GuildCount} guilds' data remain in the database.")
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task Keep()
|
||||
{
|
||||
var result = await _svc.KeepGuild(Context.Guild.Id);
|
||||
|
||||
await Response().Text("This guild's bot data will be saved.").SendAsync();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
using LinqToDB;
|
||||
using LinqToDB.Data;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using LinqToDB.Mapping;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
@@ -66,67 +67,88 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, INService
|
||||
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId))
|
||||
.DeleteAsync();
|
||||
|
||||
|
||||
// delete guild xp
|
||||
await ctx.GetTable<UserXpStats>()
|
||||
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId))
|
||||
.DeleteAsync();
|
||||
|
||||
|
||||
// delete expressions
|
||||
await ctx.GetTable<NadekoExpression>()
|
||||
.Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId.Value))
|
||||
.Where(x => x.GuildId != null
|
||||
&& !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId.Value))
|
||||
.DeleteAsync();
|
||||
|
||||
|
||||
// delete quotes
|
||||
await ctx.GetTable<Quote>()
|
||||
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId))
|
||||
.DeleteAsync();
|
||||
|
||||
|
||||
// delete planted currencies
|
||||
await ctx.GetTable<PlantedCurrency>()
|
||||
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId))
|
||||
.DeleteAsync();
|
||||
|
||||
|
||||
// delete image only channels
|
||||
await ctx.GetTable<ImageOnlyChannel>()
|
||||
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId))
|
||||
.DeleteAsync();
|
||||
|
||||
|
||||
// delete reaction roles
|
||||
await ctx.GetTable<ReactionRoleV2>()
|
||||
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId))
|
||||
.DeleteAsync();
|
||||
|
||||
|
||||
// delete ignored users
|
||||
await ctx.GetTable<DiscordPermOverride>()
|
||||
.Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId.Value))
|
||||
.Where(x => x.GuildId != null
|
||||
&& !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId.Value))
|
||||
.DeleteAsync();
|
||||
|
||||
|
||||
// delete perm overrides
|
||||
await ctx.GetTable<DiscordPermOverride>()
|
||||
.Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId.Value))
|
||||
.Where(x => x.GuildId != null
|
||||
&& !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId.Value))
|
||||
.DeleteAsync();
|
||||
|
||||
|
||||
// delete repeaters
|
||||
await ctx.GetTable<Repeater>()
|
||||
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId))
|
||||
.DeleteAsync();
|
||||
|
||||
|
||||
return new()
|
||||
{
|
||||
GuildCount = guildIds.Keys.Count,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> KeepGuild(ulong guildId)
|
||||
{
|
||||
await using var db = _db.GetDbContext();
|
||||
await using var ctx = db.CreateLinqToDBContext();
|
||||
|
||||
var table = ctx.CreateTable<KeptGuilds>(tableOptions: TableOptions.CheckExistence);
|
||||
|
||||
if (await table.AnyAsyncLinqToDB(x => x.GuildId == guildId))
|
||||
return false;
|
||||
|
||||
await table.InsertAsync(() => new()
|
||||
{
|
||||
GuildId = guildId
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private ValueTask OnKeepReport(KeepReport report)
|
||||
{
|
||||
guildIds[report.ShardId] = report.GuildIds;
|
||||
@@ -136,11 +158,18 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, INService
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
await _pubSub.Sub(_keepTriggerKey, OnKeepTrigger);
|
||||
|
||||
_client.JoinedGuild += ClientOnJoinedGuild;
|
||||
|
||||
if (_client.ShardId == 0)
|
||||
await _pubSub.Sub(_keepReportKey, OnKeepReport);
|
||||
}
|
||||
|
||||
private async Task ClientOnJoinedGuild(SocketGuild arg)
|
||||
{
|
||||
await KeepGuild(arg.Id);
|
||||
}
|
||||
|
||||
private ValueTask OnKeepTrigger(bool arg)
|
||||
{
|
||||
_pubSub.Pub(_keepReportKey,
|
||||
@@ -152,4 +181,10 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, INService
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public class KeptGuilds
|
||||
{
|
||||
[PrimaryKey]
|
||||
public ulong GuildId { get; set; }
|
||||
}
|
@@ -3,4 +3,5 @@
|
||||
public interface ICleanupService
|
||||
{
|
||||
Task<KeepResult?> DeleteMissingGuildDataAsync();
|
||||
Task<bool> KeepGuild(ulong guildId);
|
||||
}
|
@@ -225,5 +225,19 @@ public partial class Administration
|
||||
if (!enabled)
|
||||
await Response().Pending(strs.greetdmmsg_enable($"`{prefix}greetdm`")).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageGuild)]
|
||||
[Ratelimit(5)]
|
||||
public async Task BoostTest([Leftover] IGuildUser? user = null)
|
||||
{
|
||||
user ??= (IGuildUser)ctx.User;
|
||||
|
||||
await _service.BoostTest((ITextChannel)ctx.Channel, user);
|
||||
var enabled = _service.GetBoostEnabled(ctx.Guild.Id);
|
||||
if (!enabled)
|
||||
await Response().Pending(strs.boostmsg_enable($"`{prefix}boost`")).SendAsync();
|
||||
}
|
||||
}
|
||||
}
|
@@ -242,7 +242,7 @@ public class GreetService : INService, IReadyExecutor
|
||||
guild: channel.Guild,
|
||||
channel: channel,
|
||||
users: users.ToArray());
|
||||
|
||||
|
||||
var text = SmartText.CreateFrom(conf.ChannelGreetMessageText);
|
||||
text = await _repSvc.ReplaceAsync(text, repCtx);
|
||||
try
|
||||
@@ -630,6 +630,13 @@ public class GreetService : INService, IReadyExecutor
|
||||
return conf.SendChannelByeMessage;
|
||||
}
|
||||
|
||||
public bool GetBoostEnabled(ulong guildId)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var conf = uow.GuildConfigsForId(guildId, set => set);
|
||||
return conf.SendBoostMessage;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Test Messages
|
||||
|
@@ -65,23 +65,6 @@ public partial class Administration
|
||||
await progressMsg.DeleteAsync();
|
||||
}
|
||||
|
||||
private async Task SendResult(PruneResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case PruneResult.Success:
|
||||
break;
|
||||
case PruneResult.AlreadyRunning:
|
||||
break;
|
||||
case PruneResult.FeatureLimit:
|
||||
await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(result), result, null);
|
||||
}
|
||||
}
|
||||
|
||||
// prune x
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
@@ -218,5 +201,27 @@ public partial class Administration
|
||||
|
||||
await Response().Confirm(strs.prune_cancelled).SendAsync();
|
||||
}
|
||||
|
||||
|
||||
private async Task SendResult(PruneResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case PruneResult.Success:
|
||||
break;
|
||||
case PruneResult.AlreadyRunning:
|
||||
var msg = await Response().Pending(strs.prune_already_running).SendAsync();
|
||||
msg.DeleteAfter(5);
|
||||
break;
|
||||
case PruneResult.FeatureLimit:
|
||||
var msg2 = await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
|
||||
msg2.DeleteAfter(10);
|
||||
break;
|
||||
default:
|
||||
Log.Error("Unhandled result received in prune: {Result}", result);
|
||||
await Response().Error(strs.error_occured).SendAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -26,21 +26,21 @@ public class PruneService : INService
|
||||
)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(channel, nameof(channel));
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
|
||||
|
||||
var originalAmount = amount;
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
|
||||
|
||||
using var cancelSource = new CancellationTokenSource();
|
||||
if (!_pruningGuilds.TryAdd(channel.GuildId, cancelSource))
|
||||
return PruneResult.AlreadyRunning;
|
||||
|
||||
if (!await _ps.LimitHitAsync(LimitedFeatureName.Prune, channel.Guild.OwnerId))
|
||||
{
|
||||
return PruneResult.FeatureLimit;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!await _ps.LimitHitAsync(LimitedFeatureName.Prune, channel.Guild.OwnerId))
|
||||
{
|
||||
return PruneResult.FeatureLimit;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
IMessage[] msgs;
|
||||
IMessage lastMessage = null;
|
||||
|
@@ -33,7 +33,7 @@ public partial class Administration
|
||||
var msg = await ctx.Channel.GetMessageAsync(messageId);
|
||||
if (msg is null)
|
||||
{
|
||||
await Response().Error(strs.not_found).SendAsync();
|
||||
await Response().Error(strs.rero_message_not_found).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -273,6 +273,31 @@ public partial class Administration
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public Task WarnDelete(IGuildUser user, int index)
|
||||
=> WarnDelete(user.Id, index);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task WarnDelete(ulong userId, int index)
|
||||
{
|
||||
if (--index < 0)
|
||||
return;
|
||||
|
||||
var warn = await _service.WarnDelete(userId, index);
|
||||
|
||||
if (warn is null)
|
||||
{
|
||||
await Response().Error(strs.warning_not_found).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await Response().Confirm(strs.warning_deleted(Format.Bold(index.ToString()))).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.BanMembers)]
|
||||
@@ -286,6 +311,7 @@ public partial class Administration
|
||||
{
|
||||
if (index < 0)
|
||||
return;
|
||||
|
||||
var success = await _service.WarnClearAsync(ctx.Guild.Id, userId, index, ctx.User.ToString());
|
||||
var userStr = Format.Bold((ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString());
|
||||
if (index == 0)
|
||||
|
@@ -89,9 +89,10 @@ public class UserPunishService : INService, IReadyExecutor
|
||||
{
|
||||
ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
|
||||
|
||||
previousCount = uow.Set<Warning>().ForId(guildId, userId)
|
||||
.Where(w => !w.Forgiven && w.UserId == userId)
|
||||
.Sum(x => x.Weight);
|
||||
previousCount = uow.Set<Warning>()
|
||||
.ForId(guildId, userId)
|
||||
.Where(w => !w.Forgiven && w.UserId == userId)
|
||||
.Sum(x => x.Weight);
|
||||
|
||||
uow.Set<Warning>().Add(warn);
|
||||
|
||||
@@ -103,7 +104,7 @@ public class UserPunishService : INService, IReadyExecutor
|
||||
var totalCount = previousCount + weight;
|
||||
|
||||
var p = ps.Where(x => x.Count > previousCount && x.Count <= totalCount)
|
||||
.MaxBy(x => x.Count);
|
||||
.MaxBy(x => x.Count);
|
||||
|
||||
if (p is not null)
|
||||
{
|
||||
@@ -244,33 +245,33 @@ public class UserPunishService : INService, IReadyExecutor
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var cleared = await uow.Set<Warning>()
|
||||
.Where(x => uow.Set<GuildConfig>()
|
||||
.Any(y => y.GuildId == x.GuildId
|
||||
&& y.WarnExpireHours > 0
|
||||
&& y.WarnExpireAction == WarnExpireAction.Clear)
|
||||
&& x.Forgiven == false
|
||||
&& x.DateAdded
|
||||
< DateTime.UtcNow.AddHours(-uow.Set<GuildConfig>()
|
||||
.Where(y => x.GuildId == y.GuildId)
|
||||
.Select(y => y.WarnExpireHours)
|
||||
.First()))
|
||||
.UpdateAsync(_ => new()
|
||||
{
|
||||
Forgiven = true,
|
||||
ForgivenBy = "expiry"
|
||||
});
|
||||
.Where(x => uow.Set<GuildConfig>()
|
||||
.Any(y => y.GuildId == x.GuildId
|
||||
&& y.WarnExpireHours > 0
|
||||
&& y.WarnExpireAction == WarnExpireAction.Clear)
|
||||
&& x.Forgiven == false
|
||||
&& x.DateAdded
|
||||
< DateTime.UtcNow.AddHours(-uow.Set<GuildConfig>()
|
||||
.Where(y => x.GuildId == y.GuildId)
|
||||
.Select(y => y.WarnExpireHours)
|
||||
.First()))
|
||||
.UpdateAsync(_ => new()
|
||||
{
|
||||
Forgiven = true,
|
||||
ForgivenBy = "expiry"
|
||||
});
|
||||
|
||||
var deleted = await uow.Set<Warning>()
|
||||
.Where(x => uow.Set<GuildConfig>()
|
||||
.Any(y => y.GuildId == x.GuildId
|
||||
&& y.WarnExpireHours > 0
|
||||
&& y.WarnExpireAction == WarnExpireAction.Delete)
|
||||
&& x.DateAdded
|
||||
< DateTime.UtcNow.AddHours(-uow.Set<GuildConfig>()
|
||||
.Where(y => x.GuildId == y.GuildId)
|
||||
.Select(y => y.WarnExpireHours)
|
||||
.First()))
|
||||
.DeleteAsync();
|
||||
.Where(x => uow.Set<GuildConfig>()
|
||||
.Any(y => y.GuildId == x.GuildId
|
||||
&& y.WarnExpireHours > 0
|
||||
&& y.WarnExpireAction == WarnExpireAction.Delete)
|
||||
&& x.DateAdded
|
||||
< DateTime.UtcNow.AddHours(-uow.Set<GuildConfig>()
|
||||
.Where(y => x.GuildId == y.GuildId)
|
||||
.Select(y => y.WarnExpireHours)
|
||||
.First()))
|
||||
.DeleteAsync();
|
||||
|
||||
if (cleared > 0 || deleted > 0)
|
||||
{
|
||||
@@ -293,21 +294,21 @@ public class UserPunishService : INService, IReadyExecutor
|
||||
if (config.WarnExpireAction == WarnExpireAction.Clear)
|
||||
{
|
||||
await uow.Set<Warning>()
|
||||
.Where(x => x.GuildId == guildId
|
||||
&& x.Forgiven == false
|
||||
&& x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours))
|
||||
.UpdateAsync(_ => new()
|
||||
{
|
||||
Forgiven = true,
|
||||
ForgivenBy = "expiry"
|
||||
});
|
||||
.Where(x => x.GuildId == guildId
|
||||
&& x.Forgiven == false
|
||||
&& x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours))
|
||||
.UpdateAsync(_ => new()
|
||||
{
|
||||
Forgiven = true,
|
||||
ForgivenBy = "expiry"
|
||||
});
|
||||
}
|
||||
else if (config.WarnExpireAction == WarnExpireAction.Delete)
|
||||
{
|
||||
await uow.Set<Warning>()
|
||||
.Where(x => x.GuildId == guildId
|
||||
&& x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours))
|
||||
.DeleteAsync();
|
||||
.Where(x => x.GuildId == guildId
|
||||
&& x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours))
|
||||
.DeleteAsync();
|
||||
}
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
@@ -425,8 +426,8 @@ public class UserPunishService : INService, IReadyExecutor
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
return uow.GuildConfigsForId(guildId, gc => gc.Include(x => x.WarnPunishments))
|
||||
.WarnPunishments.OrderBy(x => x.Count)
|
||||
.ToArray();
|
||||
.WarnPunishments.OrderBy(x => x.Count)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public (IReadOnlyCollection<(string Original, ulong? Id, string Reason)> Bans, int Missing) MassKill(
|
||||
@@ -436,20 +437,20 @@ public class UserPunishService : INService, IReadyExecutor
|
||||
var gusers = guild.Users;
|
||||
//get user objects and reasons
|
||||
var bans = people.Split("\n")
|
||||
.Select(x =>
|
||||
{
|
||||
var split = x.Trim().Split(" ");
|
||||
.Select(x =>
|
||||
{
|
||||
var split = x.Trim().Split(" ");
|
||||
|
||||
var reason = string.Join(" ", split.Skip(1));
|
||||
var reason = string.Join(" ", split.Skip(1));
|
||||
|
||||
if (ulong.TryParse(split[0], out var id))
|
||||
return (Original: split[0], Id: id, Reason: reason);
|
||||
if (ulong.TryParse(split[0], out var id))
|
||||
return (Original: split[0], Id: id, Reason: reason);
|
||||
|
||||
return (Original: split[0],
|
||||
gusers.FirstOrDefault(u => u.ToString().ToLowerInvariant() == x)?.Id,
|
||||
Reason: reason);
|
||||
})
|
||||
.ToArray();
|
||||
return (Original: split[0],
|
||||
gusers.FirstOrDefault(u => u.ToString().ToLowerInvariant() == x)?.Id,
|
||||
Reason: reason);
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
//if user is null, means that person couldn't be found
|
||||
var missing = bans.Count(x => !x.Id.HasValue);
|
||||
@@ -483,11 +484,12 @@ public class UserPunishService : INService, IReadyExecutor
|
||||
}
|
||||
else if (template is null)
|
||||
{
|
||||
uow.Set<BanTemplate>().Add(new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Text = text
|
||||
});
|
||||
uow.Set<BanTemplate>()
|
||||
.Add(new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Text = text
|
||||
});
|
||||
}
|
||||
else
|
||||
template.Text = text;
|
||||
@@ -499,31 +501,31 @@ public class UserPunishService : INService, IReadyExecutor
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
await ctx.Set<BanTemplate>()
|
||||
.ToLinqToDBTable()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Text = null,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
PruneDays = pruneDays
|
||||
},
|
||||
old => new()
|
||||
{
|
||||
PruneDays = pruneDays
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
GuildId = guildId
|
||||
});
|
||||
.ToLinqToDBTable()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Text = null,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
PruneDays = pruneDays
|
||||
},
|
||||
old => new()
|
||||
{
|
||||
PruneDays = pruneDays
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
GuildId = guildId
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<int?> GetBanPruneAsync(ulong guildId)
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
return await ctx.Set<BanTemplate>()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.Select(x => x.PruneDays)
|
||||
.FirstOrDefaultAsyncLinqToDB();
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.Select(x => x.PruneDays)
|
||||
.FirstOrDefaultAsyncLinqToDB();
|
||||
}
|
||||
|
||||
public Task<SmartText> GetBanUserDmEmbed(
|
||||
@@ -554,18 +556,18 @@ public class UserPunishService : INService, IReadyExecutor
|
||||
banReason = string.IsNullOrWhiteSpace(banReason) ? "-" : banReason;
|
||||
|
||||
var repCtx = new ReplacementContext(client, guild)
|
||||
.WithOverride("%ban.mod%", () => moderator.ToString())
|
||||
.WithOverride("%ban.mod.fullname%", () => moderator.ToString())
|
||||
.WithOverride("%ban.mod.name%", () => moderator.Username)
|
||||
.WithOverride("%ban.mod.discrim%", () => moderator.Discriminator)
|
||||
.WithOverride("%ban.user%", () => target.ToString())
|
||||
.WithOverride("%ban.user.fullname%", () => target.ToString())
|
||||
.WithOverride("%ban.user.name%", () => target.Username)
|
||||
.WithOverride("%ban.user.discrim%", () => target.Discriminator)
|
||||
.WithOverride("%reason%", () => banReason)
|
||||
.WithOverride("%ban.reason%", () => banReason)
|
||||
.WithOverride("%ban.duration%",
|
||||
() => duration?.ToString(@"d\.hh\:mm") ?? "perma");
|
||||
.WithOverride("%ban.mod%", () => moderator.ToString())
|
||||
.WithOverride("%ban.mod.fullname%", () => moderator.ToString())
|
||||
.WithOverride("%ban.mod.name%", () => moderator.Username)
|
||||
.WithOverride("%ban.mod.discrim%", () => moderator.Discriminator)
|
||||
.WithOverride("%ban.user%", () => target.ToString())
|
||||
.WithOverride("%ban.user.fullname%", () => target.ToString())
|
||||
.WithOverride("%ban.user.name%", () => target.Username)
|
||||
.WithOverride("%ban.user.discrim%", () => target.Discriminator)
|
||||
.WithOverride("%reason%", () => banReason)
|
||||
.WithOverride("%ban.reason%", () => banReason)
|
||||
.WithOverride("%ban.duration%",
|
||||
() => duration?.ToString(@"d\.hh\:mm") ?? "perma");
|
||||
|
||||
|
||||
// if template isn't set, use the old message style
|
||||
@@ -594,4 +596,24 @@ public class UserPunishService : INService, IReadyExecutor
|
||||
var output = SmartText.CreateFrom(template);
|
||||
return await _repSvc.ReplaceAsync(output, repCtx);
|
||||
}
|
||||
|
||||
public async Task<Warning> WarnDelete(ulong userId, int index)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
||||
var warn = await uow.GetTable<Warning>()
|
||||
.Where(x => x.UserId == userId)
|
||||
.OrderByDescending(x => x.DateAdded)
|
||||
.Skip(index)
|
||||
.FirstOrDefaultAsyncLinqToDB();
|
||||
|
||||
if (warn is not null)
|
||||
{
|
||||
await uow.GetTable<Warning>()
|
||||
.Where(x => x.Id == warn.Id)
|
||||
.DeleteAsync();
|
||||
}
|
||||
|
||||
return warn;
|
||||
}
|
||||
}
|
@@ -398,15 +398,14 @@ public partial class NadekoExpressions : NadekoModule<NadekoExpressionsService>
|
||||
|
||||
var serialized = _service.ExportExpressions(ctx.Guild?.Id);
|
||||
await using var stream = await serialized.ToStream();
|
||||
await ctx.Channel.SendFileAsync(stream, "exprs-export.yml");
|
||||
await ctx.User.SendFileAsync(stream, $"exprs-export_{DateTime.UtcNow:yyyy-MM-dd-HH-mm-ss}_{(ctx.Guild?.Id.ToString() ?? "global")}.yml");
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
#if GLOBAL_NADEKO
|
||||
[OwnerOnly]
|
||||
#endif
|
||||
public async Task ExprsImport([Leftover] string input = null)
|
||||
{
|
||||
// todo cooldown on public bot for 1 day, limit 100
|
||||
|
||||
if (!AdminInGuildOrOwnerInDm())
|
||||
{
|
||||
await Response().Error(strs.expr_insuff_perms).SendAsync();
|
||||
|
@@ -73,6 +73,27 @@ public partial class Gambling
|
||||
await Response().Error(strs.cant_dm).SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
public async Task BankBalance([Leftover] IUser user)
|
||||
{
|
||||
var bal = await _bank.GetBalanceAsync(user.Id);
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(GetText(strs.bank_balance_other(user.ToString(), N(bal))));
|
||||
|
||||
try
|
||||
{
|
||||
await Response().User(ctx.User).Embed(eb).SendAsync();
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await Response().Error(strs.cant_dm).SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BankTakeInternalAsync(long amount, ulong userId)
|
||||
{
|
||||
|
@@ -3,6 +3,7 @@ using NadekoBot.Modules.Gambling.Common;
|
||||
using NadekoBot.Modules.Gambling.Common.Waifu;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using NadekoBot.Db.Models;
|
||||
using TwitchLib.Api.Helix.Models.Teams;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling;
|
||||
|
||||
@@ -21,8 +22,8 @@ public partial class Gambling
|
||||
{
|
||||
var price = _service.GetResetPrice(ctx.User);
|
||||
var embed = _sender.CreateEmbed()
|
||||
.WithTitle(GetText(strs.waifu_reset_confirm))
|
||||
.WithDescription(GetText(strs.waifu_reset_price(Format.Bold(N(price)))));
|
||||
.WithTitle(GetText(strs.waifu_reset_confirm))
|
||||
.WithDescription(GetText(strs.waifu_reset_price(Format.Bold(N(price)))));
|
||||
|
||||
if (!await PromptUserConfirmAsync(embed))
|
||||
return;
|
||||
@@ -307,24 +308,26 @@ public partial class Gambling
|
||||
fansStr = "-";
|
||||
|
||||
var embed = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.waifu)
|
||||
+ " "
|
||||
+ (wi.FullName ?? name ?? targetId.ToString())
|
||||
+ " - \"the "
|
||||
+ _service.GetClaimTitle(wi.ClaimCount)
|
||||
+ "\"")
|
||||
.AddField(GetText(strs.price), N(wi.Price), true)
|
||||
.AddField(GetText(strs.claimed_by), wi.ClaimerName ?? nobody, true)
|
||||
.AddField(GetText(strs.likes), wi.AffinityName ?? nobody, true)
|
||||
.AddField(GetText(strs.changes_of_heart), $"{wi.AffinityCount} - \"the {affInfo}\"", true)
|
||||
.AddField(GetText(strs.divorces), wi.DivorceCount.ToString(), true)
|
||||
.AddField("\u200B", "\u200B", true)
|
||||
.AddField(GetText(strs.fans(fansList.Count)), fansStr, true)
|
||||
.AddField($"Waifus ({wi.ClaimCount})",
|
||||
wi.ClaimCount == 0 ? nobody : claimsStr,
|
||||
true)
|
||||
.AddField(GetText(strs.gifts), itemsStr, true);
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.waifu)
|
||||
+ " "
|
||||
+ (wi.FullName ?? name ?? targetId.ToString())
|
||||
+ " - \"the "
|
||||
+ _service.GetClaimTitle(wi.ClaimCount)
|
||||
+ "\"")
|
||||
.AddField(GetText(strs.price), N(wi.Price), true)
|
||||
.AddField(GetText(strs.claimed_by), wi.ClaimerName ?? nobody, true)
|
||||
.AddField(GetText(strs.likes), wi.AffinityName ?? nobody, true)
|
||||
.AddField(GetText(strs.changes_of_heart),
|
||||
$"{wi.AffinityCount} - \"the {affInfo}\"",
|
||||
true)
|
||||
.AddField(GetText(strs.divorces), wi.DivorceCount.ToString(), true)
|
||||
.AddField("\u200B", "\u200B", true)
|
||||
.AddField(GetText(strs.fans(fansList.Count)), fansStr, true)
|
||||
.AddField($"Waifus ({wi.ClaimCount})",
|
||||
wi.ClaimCount == 0 ? nobody : claimsStr,
|
||||
true)
|
||||
.AddField(GetText(strs.gifts), itemsStr, true);
|
||||
|
||||
await Response().Embed(embed).SendAsync();
|
||||
}
|
||||
@@ -348,7 +351,7 @@ public partial class Gambling
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var embed = _sender.CreateEmbed().WithTitle(GetText(strs.waifu_gift_shop)).WithOkColor();
|
||||
|
||||
|
||||
items
|
||||
.ToList()
|
||||
.ForEach(x => embed.AddField(
|
||||
@@ -364,30 +367,27 @@ public partial class Gambling
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[Priority(0)]
|
||||
public async Task WaifuGift(string itemName, [Leftover] IUser waifu)
|
||||
public async Task WaifuGift(MultipleWaifuItems items, [Leftover] IUser waifu)
|
||||
{
|
||||
if (waifu.Id == ctx.User.Id)
|
||||
return;
|
||||
|
||||
var allItems = _service.GetWaifuItems();
|
||||
var item = allItems.FirstOrDefault(x => x.Name.ToLowerInvariant() == itemName.ToLowerInvariant());
|
||||
if (item is null)
|
||||
{
|
||||
await Response().Error(strs.waifu_gift_not_exist).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var sucess = await _service.GiftWaifuAsync(ctx.User, waifu, item);
|
||||
var sucess = await _service.GiftWaifuAsync(ctx.User, waifu, items.Item, items.Count);
|
||||
|
||||
if (sucess)
|
||||
{
|
||||
await Response()
|
||||
.Confirm(strs.waifu_gift(Format.Bold(item + " " + item.ItemEmoji),
|
||||
.Confirm(strs.waifu_gift(Format.Bold($"{GetCountString(items)}{items.Item} {items.Item.ItemEmoji}"),
|
||||
Format.Bold(waifu.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
else
|
||||
await Response().Error(strs.not_enough(CurrencySign)).SendAsync();
|
||||
}
|
||||
|
||||
private static string GetCountString(MultipleWaifuItems items)
|
||||
=> items.Count > 1
|
||||
? $"{items.Count}x "
|
||||
: string.Empty;
|
||||
}
|
||||
}
|
@@ -7,6 +7,7 @@ using NadekoBot.Db;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using NadekoBot.Modules.Gambling.Common.Waifu;
|
||||
using SixLabors.ImageSharp;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Services;
|
||||
|
||||
@@ -89,9 +90,14 @@ public class WaifuService : INService, IReadyExecutor
|
||||
if (waifu is null)
|
||||
return settings.Waifu.MinPrice;
|
||||
|
||||
var divorces = uow.Set<WaifuUpdate>().Count(x
|
||||
=> x.Old != null && x.Old.UserId == user.Id && x.UpdateType == WaifuUpdateType.Claimed && x.New == null);
|
||||
var affs = uow.Set<WaifuUpdate>().AsQueryable()
|
||||
var divorces = uow.Set<WaifuUpdate>()
|
||||
.Count(x
|
||||
=> x.Old != null
|
||||
&& x.Old.UserId == user.Id
|
||||
&& x.UpdateType == WaifuUpdateType.Claimed
|
||||
&& x.New == null);
|
||||
var affs = uow.Set<WaifuUpdate>()
|
||||
.AsQueryable()
|
||||
.Where(w => w.User.UserId == user.Id
|
||||
&& w.UpdateType == WaifuUpdateType.AffinityChanged
|
||||
&& w.New != null)
|
||||
@@ -110,12 +116,14 @@ public class WaifuService : INService, IReadyExecutor
|
||||
if (!await _cs.RemoveAsync(user.Id, price, new("waifu", "reset")))
|
||||
return false;
|
||||
|
||||
var affs = uow.Set<WaifuUpdate>().AsQueryable()
|
||||
var affs = uow.Set<WaifuUpdate>()
|
||||
.AsQueryable()
|
||||
.Where(w => w.User.UserId == user.Id
|
||||
&& w.UpdateType == WaifuUpdateType.AffinityChanged
|
||||
&& w.New != null);
|
||||
|
||||
var divorces = uow.Set<WaifuUpdate>().AsQueryable()
|
||||
var divorces = uow.Set<WaifuUpdate>()
|
||||
.AsQueryable()
|
||||
.Where(x => x.Old != null
|
||||
&& x.Old.UserId == user.Id
|
||||
&& x.UpdateType == WaifuUpdateType.Claimed
|
||||
@@ -158,20 +166,22 @@ public class WaifuService : INService, IReadyExecutor
|
||||
result = WaifuClaimResult.NotEnoughFunds;
|
||||
else
|
||||
{
|
||||
uow.Set<WaifuInfo>().Add(w = new()
|
||||
{
|
||||
Waifu = waifu,
|
||||
Claimer = claimer,
|
||||
Affinity = null,
|
||||
Price = amount
|
||||
});
|
||||
uow.Set<WaifuUpdate>().Add(new()
|
||||
{
|
||||
User = waifu,
|
||||
Old = null,
|
||||
New = claimer,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
uow.Set<WaifuInfo>()
|
||||
.Add(w = new()
|
||||
{
|
||||
Waifu = waifu,
|
||||
Claimer = claimer,
|
||||
Affinity = null,
|
||||
Price = amount
|
||||
});
|
||||
uow.Set<WaifuUpdate>()
|
||||
.Add(new()
|
||||
{
|
||||
User = waifu,
|
||||
Old = null,
|
||||
New = claimer,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
result = WaifuClaimResult.Success;
|
||||
}
|
||||
}
|
||||
@@ -186,13 +196,14 @@ public class WaifuService : INService, IReadyExecutor
|
||||
w.Price = amount + (amount / 4);
|
||||
result = WaifuClaimResult.Success;
|
||||
|
||||
uow.Set<WaifuUpdate>().Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldClaimer,
|
||||
New = w.Claimer,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
uow.Set<WaifuUpdate>()
|
||||
.Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldClaimer,
|
||||
New = w.Claimer,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (amount >= w.Price * settings.Waifu.Multipliers.NormalClaim) // if no affinity
|
||||
@@ -206,13 +217,14 @@ public class WaifuService : INService, IReadyExecutor
|
||||
w.Price = amount;
|
||||
result = WaifuClaimResult.Success;
|
||||
|
||||
uow.Set<WaifuUpdate>().Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldClaimer,
|
||||
New = w.Claimer,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
uow.Set<WaifuUpdate>()
|
||||
.Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldClaimer,
|
||||
New = w.Claimer,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -241,29 +253,31 @@ public class WaifuService : INService, IReadyExecutor
|
||||
|
||||
remaining = await _cache.GetRatelimitAsync(GetAffinityKey(user.Id),
|
||||
30.Minutes());
|
||||
|
||||
|
||||
if (remaining is not null)
|
||||
{
|
||||
}
|
||||
else if (w is null)
|
||||
{
|
||||
var thisUser = uow.GetOrCreateUser(user);
|
||||
uow.Set<WaifuInfo>().Add(new()
|
||||
{
|
||||
Affinity = newAff,
|
||||
Waifu = thisUser,
|
||||
Price = 1,
|
||||
Claimer = null
|
||||
});
|
||||
uow.Set<WaifuInfo>()
|
||||
.Add(new()
|
||||
{
|
||||
Affinity = newAff,
|
||||
Waifu = thisUser,
|
||||
Price = 1,
|
||||
Claimer = null
|
||||
});
|
||||
success = true;
|
||||
|
||||
uow.Set<WaifuUpdate>().Add(new()
|
||||
{
|
||||
User = thisUser,
|
||||
Old = null,
|
||||
New = newAff,
|
||||
UpdateType = WaifuUpdateType.AffinityChanged
|
||||
});
|
||||
uow.Set<WaifuUpdate>()
|
||||
.Add(new()
|
||||
{
|
||||
User = thisUser,
|
||||
Old = null,
|
||||
New = newAff,
|
||||
UpdateType = WaifuUpdateType.AffinityChanged
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -272,13 +286,14 @@ public class WaifuService : INService, IReadyExecutor
|
||||
w.Affinity = newAff;
|
||||
success = true;
|
||||
|
||||
uow.Set<WaifuUpdate>().Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldAff,
|
||||
New = newAff,
|
||||
UpdateType = WaifuUpdateType.AffinityChanged
|
||||
});
|
||||
uow.Set<WaifuUpdate>()
|
||||
.Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldAff,
|
||||
New = newAff,
|
||||
UpdateType = WaifuUpdateType.AffinityChanged
|
||||
});
|
||||
}
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
@@ -301,10 +316,10 @@ public class WaifuService : INService, IReadyExecutor
|
||||
|
||||
private static TypedKey<long> GetDivorceKey(ulong userId)
|
||||
=> new($"waifu:divorce_cd:{userId}");
|
||||
|
||||
|
||||
private static TypedKey<long> GetAffinityKey(ulong userId)
|
||||
=> new($"waifu:affinity:{userId}");
|
||||
|
||||
|
||||
public async Task<(WaifuInfo, DivorceResult, long, TimeSpan?)> DivorceWaifuAsync(IUser user, ulong targetId)
|
||||
{
|
||||
DivorceResult result;
|
||||
@@ -343,13 +358,14 @@ public class WaifuService : INService, IReadyExecutor
|
||||
var oldClaimer = w.Claimer;
|
||||
w.Claimer = null;
|
||||
|
||||
uow.Set<WaifuUpdate>().Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldClaimer,
|
||||
New = null,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
uow.Set<WaifuUpdate>()
|
||||
.Add(new()
|
||||
{
|
||||
User = w.Waifu,
|
||||
Old = oldClaimer,
|
||||
New = null,
|
||||
UpdateType = WaifuUpdateType.Claimed
|
||||
});
|
||||
}
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
@@ -358,40 +374,54 @@ public class WaifuService : INService, IReadyExecutor
|
||||
return (w, result, amount, remaining);
|
||||
}
|
||||
|
||||
public async Task<bool> GiftWaifuAsync(IUser from, IUser giftedWaifu, WaifuItemModel itemObj)
|
||||
public async Task<bool> GiftWaifuAsync(
|
||||
IUser from,
|
||||
IUser giftedWaifu,
|
||||
WaifuItemModel itemObj,
|
||||
int count)
|
||||
{
|
||||
if (!await _cs.RemoveAsync(from, itemObj.Price, new("waifu", "item")))
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(count, 1, nameof(count));
|
||||
|
||||
if (!await _cs.RemoveAsync(from, itemObj.Price * count, new("waifu", "item")))
|
||||
return false;
|
||||
|
||||
var totalValue = itemObj.Price * count;
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
var w = uow.Set<WaifuInfo>().ByWaifuUserId(giftedWaifu.Id, set => set.Include(x => x.Items).Include(x => x.Claimer));
|
||||
var w = uow.Set<WaifuInfo>()
|
||||
.ByWaifuUserId(giftedWaifu.Id,
|
||||
set => set
|
||||
.Include(x => x.Items)
|
||||
.Include(x => x.Claimer));
|
||||
if (w is null)
|
||||
{
|
||||
uow.Set<WaifuInfo>().Add(w = new()
|
||||
{
|
||||
Affinity = null,
|
||||
Claimer = null,
|
||||
Price = 1,
|
||||
Waifu = uow.GetOrCreateUser(giftedWaifu)
|
||||
});
|
||||
uow.Set<WaifuInfo>()
|
||||
.Add(w = new()
|
||||
{
|
||||
Affinity = null,
|
||||
Claimer = null,
|
||||
Price = 1,
|
||||
Waifu = uow.GetOrCreateUser(giftedWaifu)
|
||||
});
|
||||
}
|
||||
|
||||
if (!itemObj.Negative)
|
||||
{
|
||||
w.Items.Add(new()
|
||||
{
|
||||
Name = itemObj.Name.ToLowerInvariant(),
|
||||
ItemEmoji = itemObj.ItemEmoji
|
||||
});
|
||||
w.Items.AddRange(Enumerable.Range(0, count)
|
||||
.Select((_) => new WaifuItem()
|
||||
{
|
||||
Name = itemObj.Name.ToLowerInvariant(),
|
||||
ItemEmoji = itemObj.ItemEmoji
|
||||
}));
|
||||
|
||||
if (w.Claimer?.UserId == from.Id)
|
||||
w.Price += (long)(itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect);
|
||||
w.Price += (long)(totalValue * _gss.Data.Waifu.Multipliers.GiftEffect);
|
||||
else
|
||||
w.Price += itemObj.Price / 2;
|
||||
w.Price += totalValue / 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
w.Price -= (long)(itemObj.Price * _gss.Data.Waifu.Multipliers.NegativeGiftEffect);
|
||||
w.Price -= (long)(totalValue * _gss.Data.Waifu.Multipliers.NegativeGiftEffect);
|
||||
if (w.Price < 1)
|
||||
w.Price = 1;
|
||||
}
|
||||
@@ -492,6 +522,7 @@ public class WaifuService : INService, IReadyExecutor
|
||||
}
|
||||
|
||||
private static readonly TypedKey<long> _waifuDecayKey = $"waifu:last_decay";
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
// only decay waifu values from shard 0
|
||||
@@ -513,7 +544,7 @@ public class WaifuService : INService, IReadyExecutor
|
||||
var nowB = now.ToBinary();
|
||||
|
||||
var result = await _cache.GetAsync(_waifuDecayKey);
|
||||
|
||||
|
||||
if (result.TryGetValue(out var val))
|
||||
{
|
||||
var lastDecay = DateTime.FromBinary(val);
|
||||
@@ -533,7 +564,6 @@ public class WaifuService : INService, IReadyExecutor
|
||||
{
|
||||
Price = (long)(old.Price * multi)
|
||||
});
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -550,33 +580,35 @@ public class WaifuService : INService, IReadyExecutor
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
return await ctx.GetTable<DiscordUser>()
|
||||
.Where(x => ctx.GetTable<WaifuInfo>()
|
||||
.Where(wi => wi.ClaimerId == waifuId)
|
||||
.Select(wi => wi.WaifuId)
|
||||
.Contains(x.Id))
|
||||
.Select(x => $"{x.Username}#{x.Discriminator}")
|
||||
.ToListAsyncEF();
|
||||
.Where(x => ctx.GetTable<WaifuInfo>()
|
||||
.Where(wi => wi.ClaimerId == waifuId)
|
||||
.Select(wi => wi.WaifuId)
|
||||
.Contains(x.Id))
|
||||
.Select(x => $"{x.Username}#{x.Discriminator}")
|
||||
.ToListAsyncEF();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<string>> GetFansNames(int waifuId)
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
return await ctx.GetTable<DiscordUser>()
|
||||
.Where(x => ctx.GetTable<WaifuInfo>()
|
||||
.Where(wi => wi.AffinityId == waifuId)
|
||||
.Select(wi => wi.WaifuId)
|
||||
.Contains(x.Id))
|
||||
.Select(x => $"{x.Username}#{x.Discriminator}")
|
||||
.ToListAsyncEF();
|
||||
.Where(x => ctx.GetTable<WaifuInfo>()
|
||||
.Where(wi => wi.AffinityId == waifuId)
|
||||
.Select(wi => wi.WaifuId)
|
||||
.Contains(x.Id))
|
||||
.Select(x => $"{x.Username}#{x.Discriminator}")
|
||||
.ToListAsyncEF();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<WaifuItem>> GetItems(int waifuId)
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
return await ctx.GetTable<WaifuItem>()
|
||||
.Where(x => x.WaifuInfoId == ctx.GetTable<WaifuInfo>()
|
||||
.Where(x => x.WaifuId == waifuId)
|
||||
.Select(x => x.Id)
|
||||
.FirstOrDefault())
|
||||
.ToListAsyncEF();
|
||||
.Where(x => x.WaifuInfoId
|
||||
== ctx.GetTable<WaifuInfo>()
|
||||
.Where(x => x.WaifuId == waifuId)
|
||||
.Select(x => x.Id)
|
||||
.FirstOrDefault())
|
||||
.ToListAsyncEF();
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling;
|
||||
|
||||
public record class MultipleWaifuItems(int Count, WaifuItemModel Item);
|
@@ -0,0 +1,47 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Common.TypeReaders;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling;
|
||||
|
||||
public partial class MultipleWaifuItemsTypeReader : NadekoTypeReader<MultipleWaifuItems>
|
||||
{
|
||||
private readonly WaifuService _service;
|
||||
|
||||
[GeneratedRegex(@"(?:(?<count>\d+)[x*])?(?<item>.+)")]
|
||||
private static partial Regex ItemRegex();
|
||||
|
||||
public MultipleWaifuItemsTypeReader(WaifuService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
public override ValueTask<TypeReaderResult<MultipleWaifuItems>> ReadAsync(ICommandContext ctx, string input)
|
||||
{
|
||||
input = input.ToLowerInvariant();
|
||||
var match = ItemRegex().Match(input);
|
||||
if (!match.Success)
|
||||
{
|
||||
return new(Discord.Commands.TypeReaderResult.FromError(CommandError.ParseFailed, "Invalid input."));
|
||||
}
|
||||
|
||||
var count = 1;
|
||||
if (match.Groups["count"].Success)
|
||||
{
|
||||
if (!int.TryParse(match.Groups["count"].Value, out count) || count < 1)
|
||||
{
|
||||
return new(Discord.Commands.TypeReaderResult.FromError(CommandError.ParseFailed, "Invalid count."));
|
||||
}
|
||||
}
|
||||
|
||||
var itemName = match.Groups["item"].Value?.ToLowerInvariant();
|
||||
var allItems = _service.GetWaifuItems();
|
||||
var item = allItems.FirstOrDefault(x => x.Name.ToLowerInvariant() == itemName);
|
||||
if (item is null)
|
||||
{
|
||||
return new(Discord.Commands.TypeReaderResult.FromError(CommandError.ParseFailed, "Waifu gift does not exist."));
|
||||
}
|
||||
|
||||
return new(Discord.Commands.TypeReaderResult.FromSuccess(new MultipleWaifuItems(count, item)));
|
||||
}
|
||||
}
|
@@ -18,32 +18,20 @@ public partial class Games
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[NoPublicBot]
|
||||
public async Task CleverBot()
|
||||
{
|
||||
var channel = (ITextChannel)ctx.Channel;
|
||||
|
||||
if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out _))
|
||||
{
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
uow.Set<GuildConfig>().SetCleverbotEnabled(ctx.Guild.Id, false);
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
var newState = await _service.ToggleChatterBotAsync(ctx.Guild.Id);
|
||||
|
||||
if (!newState)
|
||||
{
|
||||
await Response().Confirm(strs.chatbot_disabled).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
_service.ChatterBotGuilds.TryAdd(channel.Guild.Id, new(() => _service.CreateSession(), true));
|
||||
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
uow.Set<GuildConfig>().SetCleverbotEnabled(ctx.Guild.Id, true);
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await Response().Confirm(strs.chatbot_enabled).SendAsync();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,6 @@
|
||||
#nullable disable
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Games.Common;
|
||||
@@ -10,7 +12,7 @@ namespace NadekoBot.Modules.Games.Services;
|
||||
|
||||
public class ChatterBotService : IExecOnMessage
|
||||
{
|
||||
public ConcurrentDictionary<ulong, Lazy<IChatterBotSession>> ChatterBotGuilds { get; }
|
||||
private ConcurrentDictionary<ulong, Lazy<IChatterBotSession>> ChatterBotGuilds { get; }
|
||||
|
||||
public int Priority
|
||||
=> 1;
|
||||
@@ -21,6 +23,7 @@ public class ChatterBotService : IExecOnMessage
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly GamesConfigService _gcs;
|
||||
private readonly IMessageSenderService _sender;
|
||||
private readonly DbService _db;
|
||||
public readonly IPatronageService _ps;
|
||||
|
||||
public ChatterBotService(
|
||||
@@ -31,12 +34,14 @@ public class ChatterBotService : IExecOnMessage
|
||||
IHttpClientFactory factory,
|
||||
IBotCredentials creds,
|
||||
GamesConfigService gcs,
|
||||
IMessageSenderService sender)
|
||||
IMessageSenderService sender,
|
||||
DbService db)
|
||||
{
|
||||
_client = client;
|
||||
_perms = perms;
|
||||
_creds = creds;
|
||||
_sender = sender;
|
||||
_db = db;
|
||||
_httpFactory = factory;
|
||||
_perms = perms;
|
||||
_gcs = gcs;
|
||||
@@ -58,18 +63,21 @@ public class ChatterBotService : IExecOnMessage
|
||||
|
||||
Log.Information("Cleverbot will not work as the api key is missing");
|
||||
return null;
|
||||
case ChatBotImplementation.Gpt:
|
||||
case ChatBotImplementation.OpenAi:
|
||||
var data = _gcs.Data;
|
||||
if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey))
|
||||
return new OfficialGptSession(_creds.Gpt3ApiKey,
|
||||
_gcs.Data.ChatGpt.ModelName,
|
||||
_gcs.Data.ChatGpt.ChatHistory,
|
||||
_gcs.Data.ChatGpt.MaxTokens,
|
||||
_gcs.Data.ChatGpt.MinTokens,
|
||||
_gcs.Data.ChatGpt.PersonalityPrompt,
|
||||
return new OpenAiApiSession(
|
||||
data.ChatGpt.ApiUrl,
|
||||
_creds.Gpt3ApiKey,
|
||||
data.ChatGpt.ModelName,
|
||||
data.ChatGpt.ChatHistory,
|
||||
data.ChatGpt.MaxTokens,
|
||||
data.ChatGpt.MinTokens,
|
||||
data.ChatGpt.PersonalityPrompt,
|
||||
_client.CurrentUser.Username,
|
||||
_httpFactory);
|
||||
|
||||
Log.Information("Gpt3 will not work as the api key is missing");
|
||||
Log.Information("Openai Api will likely not work as the api key is missing");
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
@@ -96,6 +104,8 @@ public class ChatterBotService : IExecOnMessage
|
||||
message = msg.Content[normalMention.Length..].Trim();
|
||||
else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture))
|
||||
message = msg.Content[nickMention.Length..].Trim();
|
||||
else if (msg.ReferencedMessage?.Author.Id == nadekoId)
|
||||
message = msg.Content;
|
||||
else
|
||||
return null;
|
||||
|
||||
@@ -192,4 +202,38 @@ public class ChatterBotService : IExecOnMessage
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> ToggleChatterBotAsync(ulong guildId)
|
||||
{
|
||||
if (ChatterBotGuilds.TryRemove(guildId, out _))
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.Set<GuildConfig>()
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.UpdateAsync((gc) => new GuildConfig()
|
||||
{
|
||||
CleverbotEnabled = false
|
||||
});
|
||||
await uow.SaveChangesAsync();
|
||||
return false;
|
||||
}
|
||||
|
||||
ChatterBotGuilds.TryAdd(guildId, new(() => CreateSession(), true));
|
||||
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
await uow.Set<GuildConfig>()
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.UpdateAsync((gc) => new GuildConfig()
|
||||
{
|
||||
CleverbotEnabled = true
|
||||
});
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
9
src/NadekoBot/Modules/Games/ChatterBot/_common/Choice.cs
Normal file
9
src/NadekoBot/Modules/Games/ChatterBot/_common/Choice.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class Choice
|
||||
{
|
||||
[JsonPropertyName("message")]
|
||||
public required Message Message { get; init; }
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
#nullable disable
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class OpenAiCompletionResponse
|
||||
{
|
||||
[JsonPropertyName("choices")]
|
||||
public Choice[] Choices { get; set; }
|
||||
|
||||
[JsonPropertyName("usage")]
|
||||
public OpenAiUsageData Usage { get; set; }
|
||||
}
|
||||
|
||||
public class OpenAiUsageData
|
||||
{
|
||||
[JsonPropertyName("prompt_tokens")]
|
||||
public int PromptTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("completion_tokens")]
|
||||
public int CompletionTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("total_tokens")]
|
||||
public int TotalTokens { get; set; }
|
||||
}
|
||||
|
||||
public class Choice
|
||||
{
|
||||
[JsonPropertyName("message")]
|
||||
public Message Message { get; init; }
|
||||
}
|
||||
|
||||
public class Message {
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; init; }
|
||||
}
|
||||
|
||||
public class Gpt3ApiRequest
|
||||
{
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; init; }
|
||||
|
||||
[JsonPropertyName("messages")]
|
||||
public List<GPTMessage> Messages { get; init; }
|
||||
|
||||
[JsonPropertyName("temperature")]
|
||||
public int Temperature { get; init; }
|
||||
|
||||
[JsonPropertyName("max_tokens")]
|
||||
public int MaxTokens { get; init; }
|
||||
}
|
||||
|
||||
public class GPTMessage
|
||||
{
|
||||
[JsonPropertyName("role")]
|
||||
public string Role {get; init;}
|
||||
[JsonPropertyName("content")]
|
||||
public string Content {get; init;}
|
||||
[JsonPropertyName("name")]
|
||||
public string Name {get; init;}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class Message
|
||||
{
|
||||
[JsonPropertyName("content")]
|
||||
public required string Content { get; init; }
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class OpenAiApiMessage
|
||||
{
|
||||
[JsonPropertyName("role")]
|
||||
public required string Role { get; init; }
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public required string Content { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class OpenAiApiRequest
|
||||
{
|
||||
[JsonPropertyName("model")]
|
||||
public required string Model { get; init; }
|
||||
|
||||
[JsonPropertyName("messages")]
|
||||
public required List<OpenAiApiMessage> Messages { get; init; }
|
||||
|
||||
[JsonPropertyName("temperature")]
|
||||
public required int Temperature { get; init; }
|
||||
|
||||
[JsonPropertyName("max_tokens")]
|
||||
public required int MaxTokens { get; init; }
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class OpenAiApiUsageData
|
||||
{
|
||||
[JsonPropertyName("prompt_tokens")]
|
||||
public int PromptTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("completion_tokens")]
|
||||
public int CompletionTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("total_tokens")]
|
||||
public int TotalTokens { get; set; }
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
#nullable disable
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class OpenAiCompletionResponse
|
||||
{
|
||||
[JsonPropertyName("choices")]
|
||||
public Choice[] Choices { get; set; }
|
||||
|
||||
[JsonPropertyName("usage")]
|
||||
public OpenAiApiUsageData Usage { get; set; }
|
||||
}
|
@@ -1,16 +1,15 @@
|
||||
#nullable disable
|
||||
using Newtonsoft.Json;
|
||||
using OneOf.Types;
|
||||
using System.Net.Http.Json;
|
||||
using SharpToken;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class OfficialGptSession : IChatterBotSession
|
||||
public partial class OpenAiApiSession : IChatterBotSession
|
||||
{
|
||||
private string Uri
|
||||
=> $"https://api.openai.com/v1/chat/completions";
|
||||
|
||||
private readonly string _baseUrl;
|
||||
private readonly string _apiKey;
|
||||
private readonly string _model;
|
||||
private readonly int _maxHistory;
|
||||
@@ -18,13 +17,14 @@ public class OfficialGptSession : IChatterBotSession
|
||||
private readonly int _minTokens;
|
||||
private readonly string _nadekoUsername;
|
||||
private readonly GptEncoding _encoding;
|
||||
private List<GPTMessage> messages = new();
|
||||
private List<OpenAiApiMessage> messages = new();
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
|
||||
public OfficialGptSession(
|
||||
public OpenAiApiSession(
|
||||
string url,
|
||||
string apiKey,
|
||||
ChatGptModel model,
|
||||
string model,
|
||||
int chatHistory,
|
||||
int maxTokens,
|
||||
int minTokens,
|
||||
@@ -32,37 +32,47 @@ public class OfficialGptSession : IChatterBotSession
|
||||
string nadekoUsername,
|
||||
IHttpClientFactory factory)
|
||||
{
|
||||
_apiKey = apiKey;
|
||||
_httpFactory = factory;
|
||||
|
||||
_model = model switch
|
||||
if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out _))
|
||||
{
|
||||
ChatGptModel.Gpt35Turbo => "gpt-3.5-turbo",
|
||||
ChatGptModel.Gpt4o => "gpt-4o",
|
||||
_ => throw new ArgumentException("Unknown, unsupported or obsolete model", nameof(model))
|
||||
};
|
||||
throw new ArgumentException("Invalid OpenAi api url provided", nameof(url));
|
||||
}
|
||||
|
||||
_baseUrl = url.TrimEnd('/');
|
||||
|
||||
_apiKey = apiKey;
|
||||
_model = model;
|
||||
_httpFactory = factory;
|
||||
_maxHistory = chatHistory;
|
||||
_maxTokens = maxTokens;
|
||||
_minTokens = minTokens;
|
||||
_nadekoUsername = nadekoUsername;
|
||||
_encoding = GptEncoding.GetEncodingForModel(_model);
|
||||
messages.Add(new()
|
||||
_nadekoUsername = UsernameCleaner().Replace(nadekoUsername, "");
|
||||
_encoding = GptEncoding.GetEncodingForModel("gpt-4o");
|
||||
if (!string.IsNullOrWhiteSpace(personality))
|
||||
{
|
||||
Role = "system",
|
||||
Content = personality,
|
||||
Name = _nadekoUsername
|
||||
});
|
||||
messages.Add(new()
|
||||
{
|
||||
Role = "system",
|
||||
Content = personality,
|
||||
Name = _nadekoUsername
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[GeneratedRegex("[^a-zA-Z0-9_-]")]
|
||||
private static partial Regex UsernameCleaner();
|
||||
|
||||
public async Task<OneOf.OneOf<ThinkResult, Error<string>>> Think(string input, string username)
|
||||
{
|
||||
username = UsernameCleaner().Replace(username, "");
|
||||
|
||||
messages.Add(new()
|
||||
{
|
||||
Role = "user",
|
||||
Content = input,
|
||||
Name = username
|
||||
});
|
||||
|
||||
while (messages.Count > _maxHistory + 2)
|
||||
{
|
||||
messages.RemoveAt(1);
|
||||
@@ -83,28 +93,29 @@ public class OfficialGptSession : IChatterBotSession
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Error<string>("Token count exceeded, please increase the number of tokens in the bot config and restart.");
|
||||
return new Error<string>(
|
||||
"Token count exceeded, please increase the number of tokens in the bot config and restart.");
|
||||
}
|
||||
}
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
http.DefaultRequestHeaders.Authorization = new("Bearer", _apiKey);
|
||||
|
||||
var data = await http.PostAsJsonAsync(Uri,
|
||||
new Gpt3ApiRequest()
|
||||
|
||||
var data = await http.PostAsJsonAsync($"{_baseUrl}/v1/chat/completions",
|
||||
new OpenAiApiRequest()
|
||||
{
|
||||
Model = _model,
|
||||
Messages = messages,
|
||||
MaxTokens = _maxTokens - tokensUsed,
|
||||
Temperature = 1,
|
||||
});
|
||||
|
||||
|
||||
var dataString = await data.Content.ReadAsStringAsync();
|
||||
try
|
||||
{
|
||||
var response = JsonConvert.DeserializeObject<OpenAiCompletionResponse>(dataString);
|
||||
|
||||
Log.Information("Received response: {response} ", dataString);
|
||||
|
||||
// Log.Information("Received response: {Response} ", dataString);
|
||||
var res = response?.Choices?[0];
|
||||
var message = res?.Message?.Content;
|
||||
|
||||
@@ -112,14 +123,14 @@ public class OfficialGptSession : IChatterBotSession
|
||||
{
|
||||
return new Error<string>("ChatGpt: Received no response.");
|
||||
}
|
||||
|
||||
|
||||
messages.Add(new()
|
||||
{
|
||||
Role = "assistant",
|
||||
Content = message,
|
||||
Name = _nadekoUsername
|
||||
});
|
||||
|
||||
|
||||
return new ThinkResult()
|
||||
{
|
||||
Text = message,
|
||||
@@ -133,11 +144,4 @@ public class OfficialGptSession : IChatterBotSession
|
||||
return new Error<string>("Unexpected response received");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ThinkResult
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public int TokensIn { get; set; }
|
||||
public int TokensOut { get; set; }
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
#nullable disable
|
||||
using System.CodeDom;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public sealed class ThinkResult
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public int TokensIn { get; set; }
|
||||
public int TokensOut { get; set; }
|
||||
}
|
@@ -8,7 +8,7 @@ namespace NadekoBot.Modules.Games.Common;
|
||||
public sealed partial class GamesConfig : ICloneable<GamesConfig>
|
||||
{
|
||||
[Comment("DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 4;
|
||||
public int Version { get; set; } = 5;
|
||||
|
||||
[Comment("Hangman related settings (.hangman command)")]
|
||||
public HangmanConfig Hangman { get; set; } = new()
|
||||
@@ -103,10 +103,13 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
|
||||
}
|
||||
];
|
||||
|
||||
[Comment(@"Which chatbot API should bot use.
|
||||
'cleverbot' - bot will use Cleverbot API.
|
||||
'gpt' - bot will use GPT API")]
|
||||
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt;
|
||||
[Comment(
|
||||
"""
|
||||
Which chatbot API should bot use.
|
||||
'cleverbot' - bot will use Cleverbot API.
|
||||
'openai' - bot will use OpenAi API
|
||||
""")]
|
||||
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.OpenAi;
|
||||
|
||||
public ChatGptConfig ChatGpt { get; set; } = new();
|
||||
}
|
||||
@@ -114,19 +117,38 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
|
||||
[Cloneable]
|
||||
public sealed partial class ChatGptConfig
|
||||
{
|
||||
[Comment(@"Which GPT Model should bot use.
|
||||
gpt35turbo - cheapest
|
||||
gpt4o - more expensive, higher quality
|
||||
")]
|
||||
public ChatGptModel ModelName { get; set; } = ChatGptModel.Gpt35Turbo;
|
||||
[Comment("""
|
||||
Url to any openai api compatible url.
|
||||
Make sure to modify the modelName appropriately
|
||||
DO NOT add /v1/chat/completions suffix to the url
|
||||
""")]
|
||||
public string ApiUrl { get; set; } = "https://api.openai.com";
|
||||
|
||||
[Comment(@"How should the chat bot behave, what's its personality? (Usage of this counts towards the max tokens)")]
|
||||
public string PersonalityPrompt { get; set; } = "You are a chat bot willing to have a conversation with anyone about anything.";
|
||||
[Comment("""
|
||||
Which GPT Model should bot use.
|
||||
gpt-3.5-turbo - cheapest
|
||||
gpt-4o - more expensive, higher quality
|
||||
|
||||
[Comment(@"The maximum number of messages in a conversation that can be remembered. (This will increase the number of tokens used)")]
|
||||
If you are using another openai compatible api, you may use any of the models supported by that api
|
||||
""")]
|
||||
public string ModelName { get; set; } = "gpt-3.5-turbo";
|
||||
|
||||
[Comment("""
|
||||
How should the chatbot behave, what's its personality?
|
||||
This will be sent as a system message.
|
||||
Usage of this counts towards the max tokens.
|
||||
""")]
|
||||
public string PersonalityPrompt { get; set; } =
|
||||
"You are a chat bot willing to have a conversation with anyone about anything.";
|
||||
|
||||
[Comment(
|
||||
"""
|
||||
The maximum number of messages in a conversation that can be remembered.
|
||||
This will increase the number of tokens used.
|
||||
""")]
|
||||
public int ChatHistory { get; set; } = 5;
|
||||
|
||||
[Comment(@"The maximum number of tokens to use per GPT API call")]
|
||||
[Comment(@"The maximum number of tokens to use per OpenAi API call")]
|
||||
public int MaxTokens { get; set; } = 100;
|
||||
|
||||
[Comment(@"The minimum number of tokens to use per GPT API call, such that chat history is removed to make room.")]
|
||||
@@ -147,9 +169,9 @@ public sealed partial class TriviaConfig
|
||||
public long CurrencyReward { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Users won't be able to start trivia games which have
|
||||
a smaller win requirement than the one specified by this setting.
|
||||
""")]
|
||||
Users won't be able to start trivia games which have
|
||||
a smaller win requirement than the one specified by this setting.
|
||||
""")]
|
||||
public int MinimumWinReq { get; set; } = 1;
|
||||
}
|
||||
|
||||
@@ -163,18 +185,11 @@ public sealed partial class RaceAnimal
|
||||
public enum ChatBotImplementation
|
||||
{
|
||||
Cleverbot,
|
||||
OpenAi = 1,
|
||||
|
||||
[Obsolete]
|
||||
Gpt = 1,
|
||||
|
||||
[Obsolete]
|
||||
Gpt3 = 1,
|
||||
}
|
||||
|
||||
public enum ChatGptModel
|
||||
{
|
||||
[Obsolete]
|
||||
Gpt4,
|
||||
[Obsolete]
|
||||
Gpt432k,
|
||||
|
||||
Gpt35Turbo,
|
||||
Gpt4o,
|
||||
}
|
@@ -32,29 +32,21 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
|
||||
gs => gs.ChatBot,
|
||||
ConfigParsers.InsensitiveEnum,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
AddParsedProp("gpt.apiUrl",
|
||||
gs => gs.ChatGpt.ApiUrl,
|
||||
ConfigParsers.String,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
AddParsedProp("gpt.modelName",
|
||||
gs => gs.ChatGpt.ModelName,
|
||||
ConfigParsers.InsensitiveEnum,
|
||||
ConfigParsers.String,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
AddParsedProp("gpt.personality",
|
||||
gs => gs.ChatGpt.PersonalityPrompt,
|
||||
ConfigParsers.String,
|
||||
ConfigPrinters.ToString);
|
||||
AddParsedProp("gpt.chathistory",
|
||||
gs => gs.ChatGpt.ChatHistory,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString,
|
||||
val => val > 0);
|
||||
AddParsedProp("gpt.max_tokens",
|
||||
gs => gs.ChatGpt.MaxTokens,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString,
|
||||
val => val > 0);
|
||||
AddParsedProp("gpt.min_tokens",
|
||||
gs => gs.ChatGpt.MinTokens,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString,
|
||||
val => val > 0);
|
||||
|
||||
Migrate();
|
||||
}
|
||||
@@ -78,7 +70,7 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 3;
|
||||
c.ChatGpt.ModelName = ChatGptModel.Gpt35Turbo;
|
||||
c.ChatGpt.ModelName = "gpt35turbo";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,11 +81,40 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
|
||||
c.Version = 4;
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
c.ChatGpt.ModelName =
|
||||
c.ChatGpt.ModelName == ChatGptModel.Gpt4 || c.ChatGpt.ModelName == ChatGptModel.Gpt432k
|
||||
? ChatGptModel.Gpt4o
|
||||
: c.ChatGpt.ModelName;
|
||||
c.ChatGpt.ModelName.Equals("gpt4", StringComparison.OrdinalIgnoreCase)
|
||||
|| c.ChatGpt.ModelName.Equals("gpt432k", StringComparison.OrdinalIgnoreCase)
|
||||
? "gpt-4o"
|
||||
: "gpt-3.5-turbo";
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
});
|
||||
}
|
||||
|
||||
if (data.Version < 5)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 5;
|
||||
c.ChatBot = c.ChatBot == ChatBotImplementation.OpenAi
|
||||
? ChatBotImplementation.OpenAi
|
||||
: c.ChatBot;
|
||||
|
||||
if (c.ChatGpt.ModelName.Equals("gpt4o", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
c.ChatGpt.ModelName = "gpt-4o";
|
||||
}
|
||||
else if (c.ChatGpt.ModelName.Equals("gpt35turbo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
c.ChatGpt.ModelName = "gpt-3.5-turbo";
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning(
|
||||
"Unknown OpenAI api model name: {ModelName}. "
|
||||
+ "It will be reset to 'gpt-3.5-turbo' only this time",
|
||||
c.ChatGpt.ModelName);
|
||||
c.ChatGpt.ModelName = "gpt-3.5-turbo";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -519,31 +519,10 @@ public sealed partial class Help : NadekoModule<HelpService>
|
||||
"https://nadekobot.readthedocs.io/en/latest/"))
|
||||
.SendAsync();
|
||||
|
||||
|
||||
private Task SelfhostAction(SocketMessageComponent smc)
|
||||
=> smc.RespondConfirmAsync(_sender,
|
||||
"""
|
||||
- In case you don't want or cannot Donate to NadekoBot project, but you
|
||||
- NadekoBot is a free and [open source](https://gitlab.com/kwoth/nadekobot) project which means you can run your own "selfhosted" instance on your computer.
|
||||
|
||||
*Keep in mind that running the bot on your computer means that the bot will be offline when you turn off your computer*
|
||||
|
||||
- You can find the selfhosting guides by using the `.guide` command and clicking on the second link that pops up.
|
||||
- If you decide to selfhost the bot, still consider [supporting the project](https://patreon.com/join/nadekobot) to keep the development going :)
|
||||
""",
|
||||
true);
|
||||
|
||||
[Cmd]
|
||||
[OnlyPublicBot]
|
||||
public async Task Donate()
|
||||
{
|
||||
var selfhostInter = _inter.Create(ctx.User.Id,
|
||||
new ButtonBuilder(
|
||||
emote: new Emoji("🖥️"),
|
||||
customId: "donate:selfhosting",
|
||||
label: "Selfhosting"),
|
||||
SelfhostAction);
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle("Thank you for considering to donate to the NadekoBot project!");
|
||||
@@ -580,7 +559,6 @@ public sealed partial class Help : NadekoModule<HelpService>
|
||||
await Response()
|
||||
.Channel(await ctx.User.CreateDMChannelAsync())
|
||||
.Embed(eb)
|
||||
.Interaction(selfhostInter)
|
||||
.SendAsync();
|
||||
|
||||
_ = ctx.OkAsync();
|
||||
|
@@ -16,12 +16,12 @@ public partial class Searches
|
||||
_stocksService = stocksService;
|
||||
_stockDrawingService = stockDrawingService;
|
||||
}
|
||||
|
||||
|
||||
[Cmd]
|
||||
public async Task Stock([Leftover]string query)
|
||||
public async Task Stock([Leftover] string query)
|
||||
{
|
||||
using var typing = ctx.Channel.EnterTypingState();
|
||||
|
||||
|
||||
var stock = await _stocksService.GetStockDataAsync(query);
|
||||
|
||||
if (stock is null)
|
||||
@@ -36,9 +36,9 @@ public partial class Searches
|
||||
|
||||
var symbol = symbols.First();
|
||||
var promptEmbed = _sender.CreateEmbed()
|
||||
.WithDescription(symbol.Description)
|
||||
.WithTitle(GetText(strs.did_you_mean(symbol.Symbol)));
|
||||
|
||||
.WithDescription(symbol.Description)
|
||||
.WithTitle(GetText(strs.did_you_mean(symbol.Symbol)));
|
||||
|
||||
if (!await PromptUserConfirmAsync(promptEmbed))
|
||||
return;
|
||||
|
||||
@@ -54,7 +54,7 @@ public partial class Searches
|
||||
|
||||
var candles = await _stocksService.GetCandleDataAsync(query);
|
||||
var stockImageTask = _stockDrawingService.GenerateCombinedChartAsync(candles);
|
||||
|
||||
|
||||
var localCulture = (CultureInfo)Culture.Clone();
|
||||
localCulture.NumberFormat.CurrencySymbol = "$";
|
||||
|
||||
@@ -64,34 +64,34 @@ public partial class Searches
|
||||
|
||||
var change = (stock.Price - stock.Close).ToString("N2", Culture);
|
||||
var changePercent = (1 - (stock.Close / stock.Price)).ToString("P1", Culture);
|
||||
|
||||
|
||||
var sign50 = stock.Change50d >= 0
|
||||
? "\\🔼"
|
||||
: "\\🔻";
|
||||
|
||||
var change50 = (stock.Change50d).ToString("P1", Culture);
|
||||
|
||||
|
||||
var sign200 = stock.Change200d >= 0
|
||||
? "\\🔼"
|
||||
: "\\🔻";
|
||||
|
||||
|
||||
var change200 = (stock.Change200d).ToString("P1", Culture);
|
||||
|
||||
|
||||
var price = stock.Price.ToString("C2", localCulture);
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(stock.Symbol)
|
||||
.WithUrl($"https://www.tradingview.com/chart/?symbol={stock.Symbol}")
|
||||
.WithTitle(stock.Name)
|
||||
.AddField(GetText(strs.price), $"{sign} **{price}**", true)
|
||||
.AddField(GetText(strs.market_cap), stock.MarketCap, true)
|
||||
.AddField(GetText(strs.volume_24h), stock.DailyVolume.ToString("C0", localCulture), true)
|
||||
.AddField("Change", $"{change} ({changePercent})", true)
|
||||
// .AddField("Change 50d", $"{sign50}{change50}", true)
|
||||
// .AddField("Change 200d", $"{sign200}{change200}", true)
|
||||
.WithFooter(stock.Exchange);
|
||||
|
||||
.WithOkColor()
|
||||
.WithAuthor(stock.Symbol)
|
||||
.WithUrl($"https://www.tradingview.com/chart/?symbol={stock.Symbol}")
|
||||
.WithTitle(stock.Name)
|
||||
.AddField(GetText(strs.price), $"{sign} **{price}**", true)
|
||||
.AddField(GetText(strs.market_cap), stock.MarketCap, true)
|
||||
.AddField(GetText(strs.volume_24h), stock.DailyVolume.ToString("C0", localCulture), true)
|
||||
.AddField("Change", $"{change} ({changePercent})", true)
|
||||
// .AddField("Change 50d", $"{sign50}{change50}", true)
|
||||
// .AddField("Change 200d", $"{sign200}{change200}", true)
|
||||
.WithFooter(stock.Exchange);
|
||||
|
||||
var message = await Response().Embed(eb).SendAsync();
|
||||
await using var imageData = await stockImageTask;
|
||||
if (imageData is null)
|
||||
@@ -105,15 +105,12 @@ public partial class Searches
|
||||
await message.ModifyAsync(mp =>
|
||||
{
|
||||
mp.Attachments =
|
||||
new(new[]
|
||||
{
|
||||
attachment
|
||||
});
|
||||
new(new[] { attachment });
|
||||
|
||||
mp.Embed = eb.WithImageUrl($"attachment://{fileName}").Build();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Cmd]
|
||||
public async Task Crypto(string name)
|
||||
@@ -128,9 +125,9 @@ public partial class Searches
|
||||
if (nearest is not null)
|
||||
{
|
||||
var embed = _sender.CreateEmbed()
|
||||
.WithTitle(GetText(strs.crypto_not_found))
|
||||
.WithDescription(
|
||||
GetText(strs.did_you_mean(Format.Bold($"{nearest.Name} ({nearest.Symbol})"))));
|
||||
.WithTitle(GetText(strs.crypto_not_found))
|
||||
.WithDescription(
|
||||
GetText(strs.did_you_mean(Format.Bold($"{nearest.Name} ({nearest.Symbol})"))));
|
||||
|
||||
if (await PromptUserConfirmAsync(embed))
|
||||
crypto = nearest;
|
||||
@@ -146,7 +143,7 @@ public partial class Searches
|
||||
|
||||
var localCulture = (CultureInfo)Culture.Clone();
|
||||
localCulture.NumberFormat.CurrencySymbol = "$";
|
||||
|
||||
|
||||
var sevenDay = (usd.PercentChange7d / 100).ToString("P2", localCulture);
|
||||
var lastDay = (usd.PercentChange24h / 100).ToString("P2", localCulture);
|
||||
var price = usd.Price < 0.01
|
||||
@@ -159,28 +156,29 @@ public partial class Searches
|
||||
|
||||
await using var sparkline = await _service.GetSparklineAsync(crypto.Id, usd.PercentChange7d >= 0);
|
||||
var fileName = $"{crypto.Slug}_7d.png";
|
||||
|
||||
|
||||
var toSend = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor($"#{crypto.CmcRank}")
|
||||
.WithTitle($"{crypto.Name} ({crypto.Symbol})")
|
||||
.WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/")
|
||||
.WithThumbnailUrl($"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
|
||||
.AddField(GetText(strs.market_cap), marketCap, true)
|
||||
.AddField(GetText(strs.price), price, true)
|
||||
.AddField(GetText(strs.volume_24h), volume, true)
|
||||
.AddField(GetText(strs.change_7d_24h), $"{sevenDay} / {lastDay}", true)
|
||||
.AddField(GetText(strs.market_cap_dominance), dominance, true)
|
||||
.WithImageUrl($"attachment://{fileName}");
|
||||
.WithOkColor()
|
||||
.WithAuthor($"#{crypto.CmcRank}")
|
||||
.WithTitle($"{crypto.Name} ({crypto.Symbol})")
|
||||
.WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/")
|
||||
.WithThumbnailUrl(
|
||||
$"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
|
||||
.AddField(GetText(strs.market_cap), marketCap, true)
|
||||
.AddField(GetText(strs.price), price, true)
|
||||
.AddField(GetText(strs.volume_24h), volume, true)
|
||||
.AddField(GetText(strs.change_7d_24h), $"{sevenDay} / {lastDay}", true)
|
||||
.AddField(GetText(strs.market_cap_dominance), dominance, true)
|
||||
.WithImageUrl($"attachment://{fileName}");
|
||||
|
||||
if (crypto.CirculatingSupply is double cs)
|
||||
{
|
||||
var csStr = cs.ToString("N0", localCulture);
|
||||
|
||||
|
||||
if (crypto.MaxSupply is double ms)
|
||||
{
|
||||
var perc = (cs / ms).ToString("P1", localCulture);
|
||||
|
||||
|
||||
toSend.AddField(GetText(strs.circulating_supply), $"{csStr} ({perc})", true);
|
||||
}
|
||||
else
|
||||
@@ -192,5 +190,54 @@ public partial class Searches
|
||||
|
||||
await ctx.Channel.SendFileAsync(sparkline, fileName, embed: toSend.Build());
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Coins(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
return;
|
||||
|
||||
if (page > 25)
|
||||
page = 25;
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.PageItems(async (page) =>
|
||||
{
|
||||
var coins = await _service.GetTopCoins(page);
|
||||
return coins;
|
||||
})
|
||||
.PageSize(10)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var embed = _sender.CreateEmbed()
|
||||
.WithOkColor();
|
||||
|
||||
if (items.Count > 0)
|
||||
{
|
||||
foreach (var coin in items)
|
||||
{
|
||||
embed.AddField($"#{coin.MarketCapRank} {coin.Symbol} - {coin.Name}",
|
||||
$"""
|
||||
`Price:` {GetArrowEmoji(coin.PercentChange24h)} {coin.CurrentPrice.ToShortString()}$ ({GetSign(coin.PercentChange24h)}{Math.Round(coin.PercentChange24h, 2)}%)
|
||||
`MarketCap:` {coin.MarketCap.ToShortString()}$
|
||||
`Supply:` {(coin.CirculatingSupply?.ToShortString() ?? "?")} / {(coin.TotalSupply?.ToShortString() ?? "?")}
|
||||
""",
|
||||
inline: false);
|
||||
}
|
||||
}
|
||||
|
||||
return embed;
|
||||
})
|
||||
.CurrentPage(page)
|
||||
.AddFooter(false)
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private static string GetArrowEmoji(decimal value)
|
||||
=> value > 0 ? "▲" : "▼";
|
||||
|
||||
private static string GetSign(decimal value)
|
||||
=> value >= 0 ? "+" : "";
|
||||
}
|
||||
}
|
@@ -4,8 +4,10 @@ using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Xml;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
using StringExtensions = NadekoBot.Extensions.StringExtensions;
|
||||
@@ -212,4 +214,55 @@ public class CryptoService : INService
|
||||
var points = GetSparklinePointsFromSvgText(str);
|
||||
return points;
|
||||
}
|
||||
|
||||
private static TypedKey<IReadOnlyCollection<GeckoCoinsResult>> GetTopCoinsKey()
|
||||
=> new($"crypto:top_coins");
|
||||
|
||||
public async Task<IReadOnlyCollection<GeckoCoinsResult>?> GetTopCoins(int page)
|
||||
{
|
||||
if (page >= 25)
|
||||
page = 24;
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
|
||||
http.AddFakeHeaders();
|
||||
|
||||
var result = await _cache.GetOrAddAsync<IReadOnlyCollection<GeckoCoinsResult>>(GetTopCoinsKey(),
|
||||
async () => await http.GetFromJsonAsync<List<GeckoCoinsResult>>(
|
||||
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=250")
|
||||
?? [],
|
||||
expiry: TimeSpan.FromHours(1));
|
||||
|
||||
return result!.Skip(page * 10).Take(10).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GeckoCoinsResult
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public required string Id { get; init; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
[JsonPropertyName("symbol")]
|
||||
public required string Symbol { get; init; }
|
||||
|
||||
[JsonPropertyName("current_price")]
|
||||
public required decimal CurrentPrice { get; init; }
|
||||
|
||||
[JsonPropertyName("price_change_percentage_24h")]
|
||||
public required decimal PercentChange24h { get; init; }
|
||||
|
||||
[JsonPropertyName("market_cap")]
|
||||
public required decimal MarketCap { get; init; }
|
||||
|
||||
[JsonPropertyName("circulating_supply")]
|
||||
public required decimal? CirculatingSupply { get; init; }
|
||||
|
||||
[JsonPropertyName("total_supply")]
|
||||
public required decimal? TotalSupply { get; init; }
|
||||
|
||||
[JsonPropertyName("market_cap_rank")]
|
||||
public required int MarketCapRank { get; init; }
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public class BibleVerse
|
||||
{
|
||||
[JsonPropertyName("book_name")]
|
||||
public required string BookName { get; set; }
|
||||
|
||||
public required int Chapter { get; set; }
|
||||
public required int Verse { get; set; }
|
||||
public required string Text { get; set; }
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public class BibleVerses
|
||||
{
|
||||
public string? Error { get; set; }
|
||||
public BibleVerse[]? Verses { get; set; }
|
||||
}
|
20
src/NadekoBot/Modules/Searches/Religious/Common/QuranAyah.cs
Normal file
20
src/NadekoBot/Modules/Searches/Religious/Common/QuranAyah.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class QuranAyah
|
||||
{
|
||||
[JsonPropertyName("number")]
|
||||
public int Number { get; set; }
|
||||
|
||||
[JsonPropertyName("audio")]
|
||||
public string Audio { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("text")]
|
||||
public string Text { get; set; }
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class QuranResponse<T>
|
||||
{
|
||||
[JsonPropertyName("code")]
|
||||
public required int Code { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public required string Status { get; set; }
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public required T[] Data { get; set; }
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
using NadekoBot.Modules.Searches.Common;
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public sealed class ReligiousApiService : INService
|
||||
{
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
public ReligiousApiService(IHttpClientFactory httpFactory)
|
||||
{
|
||||
_httpFactory = httpFactory;
|
||||
}
|
||||
|
||||
public async Task<OneOf<BibleVerse, Error<string>>> GetBibleVerseAsync(string book, string chapterAndVerse)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(book) || string.IsNullOrWhiteSpace(chapterAndVerse))
|
||||
return new Error<string>("Invalid input.");
|
||||
|
||||
|
||||
book = Uri.EscapeDataString(book);
|
||||
chapterAndVerse = Uri.EscapeDataString(chapterAndVerse);
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
try
|
||||
{
|
||||
var res = await http.GetFromJsonAsync<BibleVerses>($"https://bible-api.com/{book} {chapterAndVerse}");
|
||||
|
||||
if (res is null || res.Error is not null || res.Verses is null || res.Verses.Length == 0)
|
||||
{
|
||||
return new Error<string>(res?.Error ?? "No verse found.");
|
||||
}
|
||||
|
||||
return res.Verses[0];
|
||||
}
|
||||
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return new Error<string>("No verse found.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OneOf<QuranResponse<QuranAyah>, Error<LocStr>>> GetQuranVerseAsync(string ayah)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ayah))
|
||||
return new Error<LocStr>(strs.invalid_input);
|
||||
|
||||
ayah = Uri.EscapeDataString(ayah);
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
var res = await http.GetFromJsonAsync<QuranResponse<QuranAyah>>(
|
||||
$"https://api.alquran.cloud/v1/ayah/{ayah}/editions/en.asad,ar.alafasy");
|
||||
|
||||
if (res is null or not { Code: 200 })
|
||||
{
|
||||
return new Error<LocStr>(strs.not_found);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
|
||||
public partial class Searches
|
||||
{
|
||||
public partial class ReligiousCommands : NadekoModule<ReligiousApiService>
|
||||
{
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
public ReligiousCommands(IHttpClientFactory httpFactory)
|
||||
=> _httpFactory = httpFactory;
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Bible(string book, string chapterAndVerse)
|
||||
{
|
||||
var res = await _service.GetBibleVerseAsync(book, chapterAndVerse);
|
||||
|
||||
if (!res.TryPickT0(out var verse, out var error))
|
||||
{
|
||||
await Response().Error(error.Value).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Embed(_sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle($"{verse.BookName} {verse.Chapter}:{verse.Verse}")
|
||||
.WithDescription(verse.Text))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Quran(string ayah)
|
||||
{
|
||||
var res = await _service.GetQuranVerseAsync(ayah);
|
||||
|
||||
if (!res.TryPickT0(out var qr, out var error))
|
||||
{
|
||||
await Response().Error(error.Value).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var english = qr.Data[0];
|
||||
var arabic = qr.Data[1];
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
await using var audio = await http.GetStreamAsync(arabic.Audio);
|
||||
|
||||
await Response()
|
||||
.Embed(_sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.AddField("Arabic", arabic.Text)
|
||||
.AddField("English", english.Text)
|
||||
.WithFooter(arabic.Number.ToString()))
|
||||
.File(audio, Uri.EscapeDataString(ayah) + ".mp3")
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,16 +2,12 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NadekoBot.Modules.Searches.Common;
|
||||
using NadekoBot.Modules.Searches.Services;
|
||||
using NadekoBot.Modules.Utility;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
|
||||
namespace NadekoBot.Modules.Searches;
|
||||
@@ -39,16 +35,6 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
_tzSvc = tzSvc;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Rip([Leftover] IGuildUser usr)
|
||||
{
|
||||
var av = usr.RealAvatarUrl();
|
||||
await using var picStream = await _service.GetRipPictureAsync(usr.Nickname ?? usr.Username, av);
|
||||
await ctx.Channel.SendFileAsync(picStream,
|
||||
"rip.png",
|
||||
$"Rip {Format.Bold(usr.ToString())} \n\t- " + Format.Italics(ctx.User.ToString()));
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Weather([Leftover] string query)
|
||||
{
|
||||
@@ -108,24 +94,7 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
var (data, err) = await _service.GetTimeDataAsync(query);
|
||||
if (err is not null)
|
||||
{
|
||||
LocStr errorKey;
|
||||
switch (err)
|
||||
{
|
||||
case TimeErrors.ApiKeyMissing:
|
||||
errorKey = strs.api_key_missing;
|
||||
break;
|
||||
case TimeErrors.InvalidInput:
|
||||
errorKey = strs.invalid_input;
|
||||
break;
|
||||
case TimeErrors.NotFound:
|
||||
errorKey = strs.not_found;
|
||||
break;
|
||||
default:
|
||||
errorKey = strs.error_occured;
|
||||
break;
|
||||
}
|
||||
|
||||
await Response().Error(errorKey).SendAsync();
|
||||
await HandleErrorAsync(err.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -136,11 +105,11 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
}
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.time_new))
|
||||
.WithDescription(Format.Code(data.Time.ToString(Culture)))
|
||||
.AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true)
|
||||
.AddField(GetText(strs.timezone), data.TimeZoneName, true);
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.time_new))
|
||||
.WithDescription(Format.Code(data.Time.ToString(Culture)))
|
||||
.AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true)
|
||||
.AddField(GetText(strs.timezone), data.TimeZoneName, true);
|
||||
|
||||
await Response().Embed(eb).SendAsync();
|
||||
}
|
||||
@@ -162,14 +131,16 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
|
||||
await Response()
|
||||
.Embed(_sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(movie.Title)
|
||||
.WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/")
|
||||
.WithDescription(movie.Plot.TrimTo(1000))
|
||||
.AddField("Rating", movie.ImdbRating, true)
|
||||
.AddField("Genre", movie.Genre, true)
|
||||
.AddField("Year", movie.Year, true)
|
||||
.WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute) ? movie.Poster : null))
|
||||
.WithOkColor()
|
||||
.WithTitle(movie.Title)
|
||||
.WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/")
|
||||
.WithDescription(movie.Plot.TrimTo(1000))
|
||||
.AddField("Rating", movie.ImdbRating, true)
|
||||
.AddField("Genre", movie.Genre, true)
|
||||
.AddField("Year", movie.Year, true)
|
||||
.WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute)
|
||||
? movie.Poster
|
||||
: null))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
@@ -196,12 +167,12 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Lmgtfy([Leftover] string ffs = null)
|
||||
public async Task Lmgtfy([Leftover] string smh = null)
|
||||
{
|
||||
if (!await ValidateQuery(ffs))
|
||||
if (!await ValidateQuery(smh))
|
||||
return;
|
||||
|
||||
var shortenedUrl = await _google.ShortenUrl($"https://letmegooglethat.com/?q={Uri.EscapeDataString(ffs)}");
|
||||
var shortenedUrl = await _google.ShortenUrl($"https://letmegooglethat.com/?q={Uri.EscapeDataString(smh)}");
|
||||
await Response().Confirm($"<{shortenedUrl}>").SendAsync();
|
||||
}
|
||||
|
||||
@@ -244,9 +215,9 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
|
||||
await Response()
|
||||
.Embed(_sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.AddField(GetText(strs.original_url), $"<{query}>")
|
||||
.AddField(GetText(strs.short_url), $"<{shortLink}>"))
|
||||
.WithOkColor()
|
||||
.AddField(GetText(strs.original_url), $"<{query}>")
|
||||
.AddField(GetText(strs.short_url), $"<{shortLink}>"))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
@@ -266,13 +237,13 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
}
|
||||
|
||||
var embed = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(card.Name)
|
||||
.WithDescription(card.Description)
|
||||
.WithImageUrl(card.ImageUrl)
|
||||
.AddField(GetText(strs.store_url), card.StoreUrl, true)
|
||||
.AddField(GetText(strs.cost), card.ManaCost, true)
|
||||
.AddField(GetText(strs.types), card.Types, true);
|
||||
.WithOkColor()
|
||||
.WithTitle(card.Name)
|
||||
.WithDescription(card.Description)
|
||||
.WithImageUrl(card.ImageUrl)
|
||||
.AddField(GetText(strs.store_url), card.StoreUrl, true)
|
||||
.AddField(GetText(strs.cost), card.ManaCost, true)
|
||||
.AddField(GetText(strs.types), card.Types, true);
|
||||
|
||||
await Response().Embed(embed).SendAsync();
|
||||
}
|
||||
@@ -331,10 +302,10 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
{
|
||||
var item = items[0];
|
||||
return _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithUrl(item.Permalink)
|
||||
.WithTitle(item.Word)
|
||||
.WithDescription(item.Definition);
|
||||
.WithOkColor()
|
||||
.WithUrl(item.Permalink)
|
||||
.WithTitle(item.Word)
|
||||
.WithDescription(item.Definition);
|
||||
})
|
||||
.SendAsync();
|
||||
return;
|
||||
@@ -354,100 +325,82 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
if (!await ValidateQuery(word))
|
||||
return;
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
string res;
|
||||
try
|
||||
|
||||
var maybeItems = await _service.GetDefinitionsAsync(word);
|
||||
|
||||
if(!maybeItems.TryPickT0(out var defs, out var error))
|
||||
{
|
||||
res = await _cache.GetOrCreateAsync($"define_{word}",
|
||||
e =>
|
||||
{
|
||||
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
|
||||
return http.GetStringAsync("https://api.pearson.com/v2/dictionaries/entries?headword="
|
||||
+ WebUtility.UrlEncode(word));
|
||||
});
|
||||
|
||||
var responseModel = JsonConvert.DeserializeObject<DefineModel>(res);
|
||||
|
||||
var data = responseModel.Results
|
||||
.Where(x => x.Senses is not null
|
||||
&& x.Senses.Count > 0
|
||||
&& x.Senses[0].Definition is not null)
|
||||
.Select(x => (Sense: x.Senses[0], x.PartOfSpeech))
|
||||
.ToList();
|
||||
|
||||
if (!data.Any())
|
||||
{
|
||||
Log.Warning("Definition not found: {Word}", word);
|
||||
await Response().Error(strs.define_unknown).SendAsync();
|
||||
}
|
||||
|
||||
|
||||
var col = data.Select(x => (
|
||||
Definition: x.Sense.Definition is string
|
||||
? x.Sense.Definition.ToString()
|
||||
: ((JArray)JToken.Parse(x.Sense.Definition.ToString())).First.ToString(),
|
||||
Example: x.Sense.Examples is null || x.Sense.Examples.Count == 0
|
||||
? string.Empty
|
||||
: x.Sense.Examples[0].Text, Word: word,
|
||||
WordType: string.IsNullOrWhiteSpace(x.PartOfSpeech) ? "-" : x.PartOfSpeech))
|
||||
.ToList();
|
||||
|
||||
Log.Information("Sending {Count} definition for: {Word}", col.Count, word);
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(col)
|
||||
.PageSize(1)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var model = items.First();
|
||||
var embed = _sender.CreateEmbed()
|
||||
.WithDescription(ctx.User.Mention)
|
||||
.AddField(GetText(strs.word), model.Word, true)
|
||||
.AddField(GetText(strs._class), model.WordType, true)
|
||||
.AddField(GetText(strs.definition), model.Definition)
|
||||
.WithOkColor();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(model.Example))
|
||||
embed.AddField(GetText(strs.example), model.Example);
|
||||
|
||||
return embed;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error retrieving definition data for: {Word}", word);
|
||||
await HandleErrorAsync(error);
|
||||
return;
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(defs)
|
||||
.PageSize(1)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var model = items.First();
|
||||
var embed = _sender.CreateEmbed()
|
||||
.WithDescription(ctx.User.Mention)
|
||||
.AddField(GetText(strs.word), model.Word, true)
|
||||
.AddField(GetText(strs._class), model.WordType, true)
|
||||
.AddField(GetText(strs.definition), model.Definition)
|
||||
.WithOkColor();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(model.Example))
|
||||
embed.AddField(GetText(strs.example), model.Example);
|
||||
|
||||
return embed;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Catfact()
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
var response = await http.GetStringAsync("https://catfact.ninja/fact");
|
||||
var maybeFact = await _service.GetCatFactAsync();
|
||||
|
||||
if (!maybeFact.TryPickT0(out var fact, out var error))
|
||||
{
|
||||
await HandleErrorAsync(error);
|
||||
return;
|
||||
}
|
||||
|
||||
var fact = JObject.Parse(response)["fact"].ToString();
|
||||
await Response().Confirm("🐈" + GetText(strs.catfact), fact).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Wiki([Leftover] string query = null)
|
||||
public async Task Wiki([Leftover] string query)
|
||||
{
|
||||
query = query?.Trim();
|
||||
|
||||
if (!await ValidateQuery(query))
|
||||
return;
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
var result = await http.GetStringAsync(
|
||||
"https://en.wikipedia.org//w/api.php?action=query&format=json&prop=info&redirects=1&formatversion=2&inprop=url&titles="
|
||||
+ Uri.EscapeDataString(query));
|
||||
var data = JsonConvert.DeserializeObject<WikipediaApiModel>(result);
|
||||
if (data.Query.Pages[0].Missing || string.IsNullOrWhiteSpace(data.Query.Pages[0].FullUrl))
|
||||
await Response().Error(strs.wiki_page_not_found).SendAsync();
|
||||
else
|
||||
await Response().Text(data.Query.Pages[0].FullUrl).SendAsync();
|
||||
var maybeRes = await _service.GetWikipediaPageAsync(query);
|
||||
if (!maybeRes.TryPickT0(out var res, out var error))
|
||||
{
|
||||
await HandleErrorAsync(error);
|
||||
return;
|
||||
}
|
||||
|
||||
var data = res.Data;
|
||||
await Response().Text(data.Url).SendAsync();
|
||||
}
|
||||
|
||||
public Task<IUserMessage> HandleErrorAsync(ErrorType error)
|
||||
{
|
||||
var errorKey = error switch
|
||||
{
|
||||
ErrorType.ApiKeyMissing => strs.api_key_missing,
|
||||
ErrorType.InvalidInput => strs.invalid_input,
|
||||
ErrorType.NotFound => strs.not_found,
|
||||
ErrorType.Unknown => strs.error_occured,
|
||||
_ => strs.error_occured,
|
||||
};
|
||||
|
||||
return Response().Error(errorKey).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
@@ -473,18 +426,17 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Avatar([Leftover] IGuildUser usr = null)
|
||||
{
|
||||
if (usr is null)
|
||||
usr = (IGuildUser)ctx.User;
|
||||
usr ??= (IGuildUser)ctx.User;
|
||||
|
||||
var avatarUrl = usr.RealAvatarUrl(2048);
|
||||
|
||||
await Response()
|
||||
.Embed(
|
||||
_sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.AddField("Username", usr.ToString())
|
||||
.AddField("Avatar Url", avatarUrl)
|
||||
.WithThumbnailUrl(avatarUrl.ToString()))
|
||||
.WithOkColor()
|
||||
.AddField("Username", usr.ToString())
|
||||
.AddField("Avatar Url", avatarUrl)
|
||||
.WithThumbnailUrl(avatarUrl.ToString()))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
@@ -497,63 +449,16 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.Channel.TriggerTypingAsync();
|
||||
using var http = _httpFactory.CreateClient();
|
||||
http.DefaultRequestHeaders.Clear();
|
||||
try
|
||||
{
|
||||
var res = await http.GetStringAsync($"https://{Uri.EscapeDataString(target)}.fandom.com/api.php"
|
||||
+ "?action=query"
|
||||
+ "&format=json"
|
||||
+ "&list=search"
|
||||
+ $"&srsearch={Uri.EscapeDataString(query)}"
|
||||
+ "&srlimit=1");
|
||||
var items = JObject.Parse(res);
|
||||
var title = items["query"]?["search"]?.FirstOrDefault()?["title"]?.ToString();
|
||||
var maybeRes = await _service.GetWikiaPageAsync(target, query);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
await Response().Error(strs.wikia_error).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var url = Uri.EscapeDataString($"https://{target}.fandom.com/wiki/{title}");
|
||||
var response = $@"`{GetText(strs.title)}` {title.SanitizeMentions()}
|
||||
`{GetText(strs.url)}:` {url}";
|
||||
await Response().Text(response).SendAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await Response().Error(strs.wikia_error).SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Bible(string book, string chapterAndVerse)
|
||||
{
|
||||
var obj = new BibleVerses();
|
||||
try
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
obj = await http.GetFromJsonAsync<BibleVerses>($"https://bible-api.com/{book} {chapterAndVerse}");
|
||||
}
|
||||
catch
|
||||
if (!maybeRes.TryPickT0(out var res, out var error))
|
||||
{
|
||||
await HandleErrorAsync(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (obj.Error is not null || obj.Verses is null || obj.Verses.Length == 0)
|
||||
await Response().Error(obj.Error ?? "No verse found.").SendAsync();
|
||||
else
|
||||
{
|
||||
var v = obj.Verses[0];
|
||||
await Response()
|
||||
.Embed(_sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle($"{v.BookName} {v.Chapter}:{v.Verse}")
|
||||
.WithDescription(v.Text))
|
||||
.SendAsync();
|
||||
}
|
||||
var response = $"### {res.Title}\n{res.Url}";
|
||||
await Response().Text(response).Sanitize().SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
@@ -571,16 +476,6 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
return;
|
||||
}
|
||||
|
||||
//var embed = _sender.CreateEmbed()
|
||||
// .WithOkColor()
|
||||
// .WithDescription(gameData.ShortDescription)
|
||||
// .WithTitle(gameData.Name)
|
||||
// .WithUrl(gameData.Link)
|
||||
// .WithImageUrl(gameData.HeaderImage)
|
||||
// .AddField(GetText(strs.genres), gameData.TotalEpisodes.ToString(), true)
|
||||
// .AddField(GetText(strs.price), gameData.IsFree ? GetText(strs.FREE) : game, true)
|
||||
// .AddField(GetText(strs.links), gameData.GetGenresString(), true)
|
||||
// .WithFooter(GetText(strs.recommendations(gameData.TotalRecommendations)));
|
||||
await Response().Text($"https://store.steampowered.com/app/{appId}").SendAsync();
|
||||
}
|
||||
|
||||
|
@@ -2,13 +2,8 @@
|
||||
using NadekoBot.Modules.Searches.Common;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SixLabors.Fonts;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
using System.Text.Json;
|
||||
using OneOf;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Services;
|
||||
|
||||
@@ -72,56 +67,6 @@ public class SearchesService : INService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Stream> GetRipPictureAsync(string text, Uri imgUrl)
|
||||
=> (await GetRipPictureFactory(text, imgUrl)).ToStream();
|
||||
|
||||
private void DrawAvatar(Image bg, Image avatarImage)
|
||||
=> bg.Mutate(x => x.Grayscale().DrawImage(avatarImage, new(83, 139), new GraphicsOptions()));
|
||||
|
||||
public async Task<byte[]> GetRipPictureFactory(string text, Uri avatarUrl)
|
||||
{
|
||||
using var bg = Image.Load<Rgba32>(await _imgs.GetRipBgAsync());
|
||||
var result = await _c.GetImageDataAsync(avatarUrl);
|
||||
if (!result.TryPickT0(out var data, out _))
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
data = await http.GetByteArrayAsync(avatarUrl);
|
||||
using (var avatarImg = Image.Load<Rgba32>(data))
|
||||
{
|
||||
avatarImg.Mutate(x => x.Resize(85, 85).ApplyRoundedCorners(42));
|
||||
await using var avStream = await avatarImg.ToStreamAsync();
|
||||
data = avStream.ToArray();
|
||||
DrawAvatar(bg, avatarImg);
|
||||
}
|
||||
|
||||
await _c.SetImageDataAsync(avatarUrl, data);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var avatarImg = Image.Load<Rgba32>(data);
|
||||
DrawAvatar(bg, avatarImg);
|
||||
}
|
||||
|
||||
bg.Mutate(x => x.DrawText(
|
||||
new TextOptions(_fonts.RipFont)
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
FallbackFontFamilies = _fonts.FallBackFonts,
|
||||
Origin = new(bg.Width / 2, 225),
|
||||
},
|
||||
text,
|
||||
Color.Black));
|
||||
|
||||
//flowa
|
||||
using (var flowers = Image.Load(await _imgs.GetRipOverlayAsync()))
|
||||
{
|
||||
bg.Mutate(x => x.DrawImage(flowers, new(0, 0), new GraphicsOptions()));
|
||||
}
|
||||
|
||||
await using var stream = bg.ToStream();
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
public async Task<WeatherData> GetWeatherDataAsync(string query)
|
||||
{
|
||||
query = query.Trim().ToLowerInvariant();
|
||||
@@ -153,26 +98,26 @@ public class SearchesService : INService
|
||||
}
|
||||
}
|
||||
|
||||
public Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataAsync(string arg)
|
||||
public Task<((string Address, DateTime Time, string TimeZoneName), ErrorType?)> GetTimeDataAsync(string arg)
|
||||
=> GetTimeDataFactory(arg);
|
||||
|
||||
//return _cache.GetOrAddCachedDataAsync($"nadeko_time_{arg}",
|
||||
// GetTimeDataFactory,
|
||||
// arg,
|
||||
// TimeSpan.FromMinutes(1));
|
||||
private async Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataFactory(
|
||||
private async Task<((string Address, DateTime Time, string TimeZoneName), ErrorType?)> GetTimeDataFactory(
|
||||
string query)
|
||||
{
|
||||
query = query.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(query))
|
||||
return (default, TimeErrors.InvalidInput);
|
||||
return (default, ErrorType.InvalidInput);
|
||||
|
||||
|
||||
var locIqKey = _creds.GetCreds().LocationIqApiKey;
|
||||
var tzDbKey = _creds.GetCreds().TimezoneDbApiKey;
|
||||
if (string.IsNullOrWhiteSpace(locIqKey) || string.IsNullOrWhiteSpace(tzDbKey))
|
||||
return (default, TimeErrors.ApiKeyMissing);
|
||||
return (default, ErrorType.ApiKeyMissing);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -196,7 +141,7 @@ public class SearchesService : INService
|
||||
if (responses is null || responses.Length == 0)
|
||||
{
|
||||
Log.Warning("Geocode lookup failed for: {Query}", query);
|
||||
return (default, TimeErrors.NotFound);
|
||||
return (default, ErrorType.NotFound);
|
||||
}
|
||||
|
||||
var geoData = responses[0];
|
||||
@@ -220,7 +165,7 @@ public class SearchesService : INService
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Weather error: {Message}", ex.Message);
|
||||
return (default, TimeErrors.NotFound);
|
||||
return (default, ErrorType.NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,12 +340,11 @@ public class SearchesService : INService
|
||||
private async Task<OmdbMovie> GetMovieDataFactory(string name)
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
var res = await http.GetStringAsync(string.Format("https://omdbapi.nadeko.bot/"
|
||||
+ "?t={0}"
|
||||
+ "&y="
|
||||
+ "&plot=full"
|
||||
+ "&r=json",
|
||||
name.Trim().Replace(' ', '+')));
|
||||
var res = await http.GetStringAsync("https://omdbapi.nadeko.bot/"
|
||||
+ $"?t={name.Trim().Replace(' ', '+')}"
|
||||
+ "&y="
|
||||
+ "&plot=full"
|
||||
+ "&r=json");
|
||||
var movie = JsonConvert.DeserializeObject<OmdbMovie>(res);
|
||||
if (movie?.Title is null)
|
||||
return null;
|
||||
@@ -454,4 +398,185 @@ public class SearchesService : INService
|
||||
|
||||
return gamesMap[key];
|
||||
}
|
||||
|
||||
public async Task<OneOf<WikipediaReply, ErrorType>> GetWikipediaPageAsync(string query)
|
||||
{
|
||||
query = query.Trim();
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
return ErrorType.InvalidInput;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _c.GetOrAddAsync($"wikipedia_{query}",
|
||||
async () =>
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
http.DefaultRequestHeaders.Clear();
|
||||
|
||||
return await http.GetStringAsync(
|
||||
"https://en.wikipedia.org/w/api.php?action=query"
|
||||
+ "&format=json"
|
||||
+ "&prop=info"
|
||||
+ "&redirects=1"
|
||||
+ "&formatversion=2"
|
||||
+ "&inprop=url"
|
||||
+ "&titles="
|
||||
+ Uri.EscapeDataString(query));
|
||||
},
|
||||
TimeSpan.FromHours(1))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var data = JsonConvert.DeserializeObject<WikipediaApiModel>(result);
|
||||
|
||||
if (data.Query.Pages is null || !data.Query.Pages.Any() || data.Query.Pages.First().Missing)
|
||||
{
|
||||
return ErrorType.NotFound;
|
||||
}
|
||||
|
||||
Log.Information("Sending wikipedia url for: {Query}", query);
|
||||
|
||||
return new WikipediaReply
|
||||
{
|
||||
Data = new()
|
||||
{
|
||||
Url = data.Query.Pages[0].FullUrl,
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error retrieving wikipedia data for: '{Query}'", query);
|
||||
|
||||
return ErrorType.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<OneOf<string, ErrorType>> GetCatFactAsync()
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
var response = await http.GetStringAsync("https://catfact.ninja/fact").ConfigureAwait(false);
|
||||
|
||||
var doc = JsonDocument.Parse(response);
|
||||
|
||||
|
||||
if (!doc.RootElement.TryGetProperty("fact", out var factElement))
|
||||
{
|
||||
return ErrorType.Unknown;
|
||||
}
|
||||
|
||||
return factElement.ToString();
|
||||
}
|
||||
|
||||
public async Task<OneOf<WikiaResponse, ErrorType>> GetWikiaPageAsync(string target, string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(target) || string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return ErrorType.InvalidInput;
|
||||
}
|
||||
|
||||
query = Uri.EscapeDataString(query.Trim());
|
||||
target = Uri.EscapeDataString(target.Trim());
|
||||
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
return ErrorType.InvalidInput;
|
||||
}
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
http.DefaultRequestHeaders.Clear();
|
||||
try
|
||||
{
|
||||
var res = await http.GetStringAsync($"https://{Uri.EscapeDataString(target)}.fandom.com/api.php"
|
||||
+ "?action=query"
|
||||
+ "&format=json"
|
||||
+ "&list=search"
|
||||
+ $"&srsearch={Uri.EscapeDataString(query)}"
|
||||
+ "&srlimit=1");
|
||||
var items = JObject.Parse(res);
|
||||
var title = items["query"]?["search"]?.FirstOrDefault()?["title"]?.ToString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
return ErrorType.NotFound;
|
||||
}
|
||||
|
||||
var url = $"https://{target}.fandom.com/wiki/{title}";
|
||||
|
||||
return new WikiaResponse()
|
||||
{
|
||||
Url = url,
|
||||
Title = title,
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error getting wikia page: {Message}", ex.Message);
|
||||
return ErrorType.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
private static TypedKey<string> GetDefineKey(string query)
|
||||
=> new TypedKey<string>($"define_{query}");
|
||||
|
||||
public async Task<OneOf<List<DefineData>, ErrorType>> GetDefinitionsAsync(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return ErrorType.InvalidInput;
|
||||
}
|
||||
|
||||
query = Uri.EscapeDataString(query);
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
string res;
|
||||
try
|
||||
{
|
||||
res = await _c.GetOrAddAsync(GetDefineKey(query),
|
||||
async () => await http.GetStringAsync(
|
||||
$"https://api.pearson.com/v2/dictionaries/entries?headword={query}"),
|
||||
TimeSpan.FromHours(12));
|
||||
|
||||
var responseModel = JsonConvert.DeserializeObject<DefineModel>(res);
|
||||
|
||||
var data = responseModel.Results
|
||||
.Where(x => x.Senses is not null
|
||||
&& x.Senses.Count > 0
|
||||
&& x.Senses[0].Definition is not null)
|
||||
.Select(x => (Sense: x.Senses[0], x.PartOfSpeech))
|
||||
.ToList();
|
||||
|
||||
if (!data.Any())
|
||||
{
|
||||
Log.Warning("Definition not found: {Word}", query);
|
||||
return ErrorType.NotFound;
|
||||
}
|
||||
|
||||
|
||||
var items = new List<DefineData>();
|
||||
|
||||
foreach (var d in data)
|
||||
{
|
||||
items.Add(new DefineData
|
||||
{
|
||||
Definition = d.Sense.Definition is JArray { Count: > 0 } defs
|
||||
? defs[0].ToString()
|
||||
: d.Sense.Definition.ToString(),
|
||||
Example = d.Sense.Examples is null || d.Sense.Examples.Count == 0
|
||||
? string.Empty
|
||||
: d.Sense.Examples[0].Text,
|
||||
WordType = string.IsNullOrWhiteSpace(d.PartOfSpeech) ? "-" : d.PartOfSpeech,
|
||||
Word = query,
|
||||
});
|
||||
}
|
||||
|
||||
return items.OrderByDescending(x => !string.IsNullOrWhiteSpace(x.Example)).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error retrieving definition data for: {Word}", query);
|
||||
return ErrorType.Unknown;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common;
|
||||
|
||||
public class BibleVerses
|
||||
{
|
||||
public string Error { get; set; }
|
||||
public BibleVerse[] Verses { get; set; }
|
||||
}
|
||||
|
||||
public class BibleVerse
|
||||
{
|
||||
[JsonPropertyName("book_name")]
|
||||
public string BookName { get; set; }
|
||||
|
||||
public int Chapter { get; set; }
|
||||
public int Verse { get; set; }
|
||||
public string Text { get; set; }
|
||||
}
|
10
src/NadekoBot/Modules/Searches/_common/DefineData.cs
Normal file
10
src/NadekoBot/Modules/Searches/_common/DefineData.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Modules.Searches.Services;
|
||||
|
||||
public sealed class DefineData
|
||||
{
|
||||
public required string Definition { get; init; }
|
||||
public required string Example { get; init; }
|
||||
public required string WordType { get; init; }
|
||||
public required string Word { get; init; }
|
||||
}
|
9
src/NadekoBot/Modules/Searches/_common/ErrorType.cs
Normal file
9
src/NadekoBot/Modules/Searches/_common/ErrorType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace NadekoBot.Modules.Searches.Services;
|
||||
|
||||
public enum ErrorType
|
||||
{
|
||||
InvalidInput,
|
||||
NotFound,
|
||||
Unknown,
|
||||
ApiKeyMissing
|
||||
}
|
7
src/NadekoBot/Modules/Searches/_common/WikiaResponse.cs
Normal file
7
src/NadekoBot/Modules/Searches/_common/WikiaResponse.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Modules.Searches.Services;
|
||||
|
||||
public sealed class WikiaResponse
|
||||
{
|
||||
public required string Url { get; init; }
|
||||
public required string Title { get; init; }
|
||||
}
|
11
src/NadekoBot/Modules/Searches/_common/WikipediaReply.cs
Normal file
11
src/NadekoBot/Modules/Searches/_common/WikipediaReply.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace NadekoBot.Modules.Searches.Services;
|
||||
|
||||
public class WikipediaReply
|
||||
{
|
||||
public class Info
|
||||
{
|
||||
public required string Url { get; init; }
|
||||
}
|
||||
|
||||
public required Info Data { get; init; }
|
||||
}
|
148
src/NadekoBot/Modules/Utility/AfkService.cs
Normal file
148
src/NadekoBot/Modules/Utility/AfkService.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
|
||||
namespace NadekoBot.Modules.Utility;
|
||||
|
||||
public sealed class AfkService : INService, IReadyExecutor
|
||||
{
|
||||
private readonly IBotCache _cache;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly MessageSenderService _mss;
|
||||
|
||||
private static readonly TimeSpan _maxAfkDuration = 8.Hours();
|
||||
public AfkService(IBotCache cache, DiscordSocketClient client, MessageSenderService mss)
|
||||
{
|
||||
_cache = cache;
|
||||
_client = client;
|
||||
_mss = mss;
|
||||
}
|
||||
|
||||
private static TypedKey<string> GetKey(ulong userId)
|
||||
=> new($"afk:msg:{userId}");
|
||||
|
||||
public async Task<bool> SetAfkAsync(ulong userId, string text)
|
||||
{
|
||||
var added = await _cache.AddAsync(GetKey(userId), text, _maxAfkDuration, overwrite: true);
|
||||
|
||||
async Task StopAfk(SocketMessage socketMessage)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (socketMessage.Author?.Id == userId)
|
||||
{
|
||||
await _cache.RemoveAsync(GetKey(userId));
|
||||
_client.MessageReceived -= StopAfk;
|
||||
|
||||
// write the message saying afk status cleared
|
||||
|
||||
if (socketMessage.Channel is ITextChannel tc)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var msg = await _mss.Response(tc).Confirm("AFK message cleared!").SendAsync();
|
||||
|
||||
msg.DeleteAfter(5);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning("Unexpected error occurred while trying to stop afk: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
_client.MessageReceived += StopAfk;
|
||||
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(_maxAfkDuration);
|
||||
_client.MessageReceived -= StopAfk;
|
||||
});
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
public Task OnReadyAsync()
|
||||
{
|
||||
_client.MessageReceived += TryTriggerAfkMessage;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task TryTriggerAfkMessage(SocketMessage arg)
|
||||
{
|
||||
if (arg.Author.IsBot)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (arg is not IUserMessage uMsg || uMsg.Channel is not ITextChannel tc)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if ((arg.MentionedUsers.Count is 0 or > 3) && uMsg.ReferencedMessage is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var botUser = await tc.Guild.GetCurrentUserAsync();
|
||||
|
||||
var perms = botUser.GetPermissions(tc);
|
||||
|
||||
if (!perms.SendMessages)
|
||||
return;
|
||||
|
||||
ulong mentionedUserId = 0;
|
||||
|
||||
if (arg.MentionedUsers.Count <= 3)
|
||||
{
|
||||
foreach (var uid in uMsg.MentionedUserIds)
|
||||
{
|
||||
if (uid == arg.Author.Id)
|
||||
continue;
|
||||
|
||||
if (arg.Content.StartsWith($"<@{uid}>") || arg.Content.StartsWith($"<@!{uid}>"))
|
||||
{
|
||||
mentionedUserId = uid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mentionedUserId == 0)
|
||||
{
|
||||
if (uMsg.ReferencedMessage?.Author?.Id is not ulong repliedUserId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mentionedUserId = repliedUserId;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _cache.GetAsync(GetKey(mentionedUserId));
|
||||
if (result.TryPickT0(out var msg, out _))
|
||||
{
|
||||
var st = SmartText.CreateFrom(msg);
|
||||
|
||||
st = "The user is AFK: " + st;
|
||||
|
||||
var toDelete = await _mss.Response(arg.Channel)
|
||||
.Message(uMsg)
|
||||
.Text(st)
|
||||
.Sanitize(false)
|
||||
.SendAsync();
|
||||
|
||||
toDelete.DeleteAfter(30);
|
||||
}
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
Log.Warning("Error in afk service: {Message}", ex.Message);
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
@@ -251,7 +251,7 @@ public sealed class AiAssistantService
|
||||
return false;
|
||||
|
||||
await _cbs.RunChatterBot(sg, msg, channel, sess, query);
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
var commandString = GetCommandString(model);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#nullable disable
|
||||
using LinqToDB.Reflection;
|
||||
using NadekoBot.Modules.Utility.Services;
|
||||
using Newtonsoft.Json;
|
||||
using System.Diagnostics;
|
||||
@@ -7,6 +7,7 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using NadekoBot.Modules.Games.Hangman;
|
||||
using NadekoBot.Modules.Searches.Common;
|
||||
|
||||
namespace NadekoBot.Modules.Utility;
|
||||
@@ -41,6 +42,7 @@ public partial class Utility : NadekoModule
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly VerboseErrorsService _veService;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly AfkService _afkService;
|
||||
|
||||
public Utility(
|
||||
DiscordSocketClient client,
|
||||
@@ -50,7 +52,8 @@ public partial class Utility : NadekoModule
|
||||
DownloadTracker tracker,
|
||||
IHttpClientFactory httpFactory,
|
||||
VerboseErrorsService veService,
|
||||
IServiceProvider services)
|
||||
IServiceProvider services,
|
||||
AfkService afkService)
|
||||
{
|
||||
_client = client;
|
||||
_coord = coord;
|
||||
@@ -60,6 +63,7 @@ public partial class Utility : NadekoModule
|
||||
_httpFactory = httpFactory;
|
||||
_veService = veService;
|
||||
_services = services;
|
||||
_afkService = afkService;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
@@ -99,7 +103,7 @@ public partial class Utility : NadekoModule
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task WhosPlaying([Leftover] string game)
|
||||
public async Task WhosPlaying([Leftover] string? game)
|
||||
{
|
||||
game = game?.Trim().ToUpperInvariant();
|
||||
if (string.IsNullOrWhiteSpace(game))
|
||||
@@ -140,7 +144,7 @@ public partial class Utility : NadekoModule
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[Priority(0)]
|
||||
public async Task InRole(int page, [Leftover] IRole role = null)
|
||||
public async Task InRole(int page, [Leftover] IRole? role = null)
|
||||
{
|
||||
if (--page < 0)
|
||||
return;
|
||||
@@ -178,7 +182,7 @@ public partial class Utility : NadekoModule
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[Priority(1)]
|
||||
public Task InRole([Leftover] IRole role = null)
|
||||
public Task InRole([Leftover] IRole? role = null)
|
||||
=> InRole(1, role);
|
||||
|
||||
[Cmd]
|
||||
@@ -218,7 +222,7 @@ public partial class Utility : NadekoModule
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task UserId([Leftover] IGuildUser target = null)
|
||||
public async Task UserId([Leftover] IGuildUser? target = null)
|
||||
{
|
||||
var usr = target ?? ctx.User;
|
||||
await Response()
|
||||
@@ -248,7 +252,7 @@ public partial class Utility : NadekoModule
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Roles(IGuildUser target, int page = 1)
|
||||
public async Task Roles(IGuildUser? target, int page = 1)
|
||||
{
|
||||
var guild = ctx.Guild;
|
||||
|
||||
@@ -301,7 +305,7 @@ public partial class Utility : NadekoModule
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task ChannelTopic([Leftover] ITextChannel channel = null)
|
||||
public async Task ChannelTopic([Leftover] ITextChannel? channel = null)
|
||||
{
|
||||
if (channel is null)
|
||||
channel = (ITextChannel)ctx.Channel;
|
||||
@@ -382,7 +386,7 @@ public partial class Utility : NadekoModule
|
||||
[BotPerm(GuildPerm.ManageEmojisAndStickers)]
|
||||
[UserPerm(GuildPerm.ManageEmojisAndStickers)]
|
||||
[Priority(0)]
|
||||
public async Task EmojiAdd(string name, string url = null)
|
||||
public async Task EmojiAdd(string name, string? url = null)
|
||||
{
|
||||
name = name.Trim(':');
|
||||
|
||||
@@ -456,50 +460,85 @@ public partial class Utility : NadekoModule
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[BotPerm(GuildPerm.ManageEmojisAndStickers)]
|
||||
[UserPerm(GuildPerm.ManageEmojisAndStickers)]
|
||||
public async Task StickerAdd(string name = null, string description = null, params string[] tags)
|
||||
public async Task StickerAdd(string? name = null, string? description = null, params string[] tags)
|
||||
{
|
||||
string format;
|
||||
Stream stream;
|
||||
|
||||
if (ctx.Message.Stickers.Count is 1 && ctx.Message.Stickers.First() is SocketSticker ss)
|
||||
{
|
||||
name ??= ss.Name;
|
||||
description = ss.Description;
|
||||
tags = tags is null or { Length: 0 } ? ss.Tags.ToArray() : tags;
|
||||
format = FormatToExtension(ss.Format);
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
stream = await http.GetStreamAsync(ss.GetStickerUrl());
|
||||
}
|
||||
else
|
||||
{
|
||||
await Response().Error(strs.sticker_error).SendAsync();
|
||||
return;
|
||||
}
|
||||
Stream? stream = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (tags.Length == 0)
|
||||
tags = [name];
|
||||
if (ctx.Message.Stickers.Count is 1 && ctx.Message.Stickers.First() is SocketSticker ss)
|
||||
{
|
||||
name ??= ss.Name;
|
||||
description = ss.Description;
|
||||
tags = tags is null or { Length: 0 } ? ss.Tags.ToArray() : tags;
|
||||
format = FormatToExtension(ss.Format);
|
||||
|
||||
await ctx.Guild.CreateStickerAsync(
|
||||
name,
|
||||
stream,
|
||||
$"{name}.{format}",
|
||||
tags,
|
||||
string.IsNullOrWhiteSpace(description) ? "Missing description" : description
|
||||
);
|
||||
using var http = _httpFactory.CreateClient();
|
||||
stream = await http.GetStreamAsync(ss.GetStickerUrl());
|
||||
}
|
||||
else if (ctx.Message.Attachments.Count is 1 && name is not null)
|
||||
{
|
||||
if (tags.Length == 0)
|
||||
tags = [name];
|
||||
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error occurred while adding a sticker: {Message}", ex.Message);
|
||||
await Response().Error(strs.error_occured).SendAsync();
|
||||
if (ctx.Message.Attachments.Count != 1)
|
||||
{
|
||||
await Response().Error(strs.sticker_error).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var attach = ctx.Message.Attachments.First();
|
||||
|
||||
|
||||
if (attach.Size > 512_000 || attach.Width != 300 || attach.Height != 300)
|
||||
{
|
||||
await Response().Error(strs.sticker_error).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
format = attach.Filename
|
||||
.Split('.')
|
||||
.Last()
|
||||
.ToLowerInvariant();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(format) || (format != "png" && format != "apng"))
|
||||
{
|
||||
await Response().Error(strs.sticker_error).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
using var http = _httpFactory.CreateClient();
|
||||
stream = await http.GetStreamAsync(attach.Url);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Response().Error(strs.sticker_error).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await ctx.Guild.CreateStickerAsync(
|
||||
name,
|
||||
stream,
|
||||
$"{name}.{format}",
|
||||
tags,
|
||||
string.IsNullOrWhiteSpace(description) ? "Missing description" : description
|
||||
);
|
||||
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
catch
|
||||
(Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error occurred while adding a sticker: {Message}", ex.Message);
|
||||
await Response().Error(strs.error_occured).SendAsync();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await stream.DisposeAsync();
|
||||
await (stream?.DisposeAsync() ?? ValueTask.CompletedTask);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,6 +700,19 @@ public partial class Utility : NadekoModule
|
||||
await Response().Confirm(strs.verbose_errors_disabled).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Afk([Leftover] string text = "No reason specified.")
|
||||
{
|
||||
var succ = await _afkService.SetAfkAsync(ctx.User.Id, text);
|
||||
|
||||
if (succ)
|
||||
{
|
||||
await Response()
|
||||
.Confirm(strs.afk_set)
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[NoPublicBot]
|
||||
[OwnerOnly]
|
||||
|
@@ -123,6 +123,8 @@ public partial class Xp
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var rank = await _service.GetClubRankAsync(club.Id);
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(allUsers)
|
||||
@@ -135,6 +137,7 @@ public partial class Xp
|
||||
.WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)")))
|
||||
.AddField(GetText(strs.desc),
|
||||
string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description)
|
||||
.AddField(GetText(strs.rank), $"#{rank}", true)
|
||||
.AddField(GetText(strs.owner), club.Owner.ToString(), true)
|
||||
// .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true)
|
||||
.AddField(GetText(strs.members),
|
||||
|
@@ -23,22 +23,22 @@ public class ClubService : INService, IClubService
|
||||
{
|
||||
if (!CheckClubName(clubName))
|
||||
return ClubCreateResult.NameTooLong;
|
||||
|
||||
|
||||
//must be lvl 5 and must not be in a club already
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
var du = uow.GetOrCreateUser(user);
|
||||
var xp = new LevelStats(du.TotalXp);
|
||||
|
||||
if (xp.Level < 5)
|
||||
|
||||
if (xp.Level < 5)
|
||||
return ClubCreateResult.InsufficientLevel;
|
||||
|
||||
|
||||
if (du.ClubId is not null)
|
||||
return ClubCreateResult.AlreadyInAClub;
|
||||
|
||||
if (await uow.Set<ClubInfo>().AnyAsyncEF(x => x.Name == clubName))
|
||||
return ClubCreateResult.NameTaken;
|
||||
|
||||
|
||||
du.IsClubAdmin = true;
|
||||
du.Club = new()
|
||||
{
|
||||
@@ -53,7 +53,7 @@ public class ClubService : INService, IClubService
|
||||
|
||||
return ClubCreateResult.Success;
|
||||
}
|
||||
|
||||
|
||||
public OneOf<ClubInfo, ClubTransferError> TransferClub(IUser from, IUser newOwner)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
@@ -62,7 +62,7 @@ public class ClubService : INService, IClubService
|
||||
|
||||
if (club is null || club.Owner.UserId != from.Id)
|
||||
return ClubTransferError.NotOwner;
|
||||
|
||||
|
||||
if (!club.Members.Contains(newOwnerUser))
|
||||
return ClubTransferError.TargetNotMember;
|
||||
|
||||
@@ -72,22 +72,22 @@ public class ClubService : INService, IClubService
|
||||
uow.SaveChanges();
|
||||
return club;
|
||||
}
|
||||
|
||||
|
||||
public async Task<ToggleAdminResult> ToggleAdminAsync(IUser owner, IUser toAdmin)
|
||||
{
|
||||
if (owner.Id == toAdmin.Id)
|
||||
return ToggleAdminResult.CantTargetThyself;
|
||||
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
var club = uow.Set<ClubInfo>().GetByOwner(owner.Id);
|
||||
var adminUser = uow.GetOrCreateUser(toAdmin);
|
||||
|
||||
if (club is null)
|
||||
return ToggleAdminResult.NotOwner;
|
||||
|
||||
if(!club.Members.Contains(adminUser))
|
||||
|
||||
if (!club.Members.Contains(adminUser))
|
||||
return ToggleAdminResult.TargetNotMember;
|
||||
|
||||
|
||||
var newState = adminUser.IsClubAdmin = !adminUser.IsClubAdmin;
|
||||
await uow.SaveChangesAsync();
|
||||
return newState ? ToggleAdminResult.AddedAdmin : ToggleAdminResult.RemovedAdmin;
|
||||
@@ -99,17 +99,17 @@ public class ClubService : INService, IClubService
|
||||
var member = uow.Set<ClubInfo>().GetByMember(user.Id);
|
||||
return member;
|
||||
}
|
||||
|
||||
|
||||
public async Task<SetClubIconResult> SetClubIconAsync(ulong ownerUserId, string url)
|
||||
{
|
||||
if (url is not null)
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
using var temp = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
if (!temp.IsImage())
|
||||
|
||||
if (!temp.IsImage())
|
||||
return SetClubIconResult.InvalidFileType;
|
||||
|
||||
|
||||
if (temp.GetContentLength() > 5.Megabytes())
|
||||
return SetClubIconResult.TooLarge;
|
||||
}
|
||||
@@ -134,6 +134,18 @@ public class ClubService : INService, IClubService
|
||||
return club is not null;
|
||||
}
|
||||
|
||||
public async Task<int> GetClubRankAsync(int clubId)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
||||
var rank = await uow.Clubs
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.Xp > (uow.Clubs.First(c => c.Id == clubId).Xp))
|
||||
.CountAsyncLinqToDB();
|
||||
|
||||
return rank + 1;
|
||||
}
|
||||
|
||||
public ClubApplyResult ApplyToClub(IUser user, ClubInfo club)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
@@ -144,10 +156,10 @@ public class ClubService : INService, IClubService
|
||||
// or doesn't min minumum level requirement, can't apply
|
||||
if (du.ClubId is not null)
|
||||
return ClubApplyResult.AlreadyInAClub;
|
||||
|
||||
|
||||
if (club.Bans.Any(x => x.UserId == du.Id))
|
||||
return ClubApplyResult.Banned;
|
||||
|
||||
|
||||
if (club.Applicants.Any(x => x.UserId == du.Id))
|
||||
return ClubApplyResult.AlreadyApplied;
|
||||
|
||||
@@ -162,7 +174,7 @@ public class ClubService : INService, IClubService
|
||||
return ClubApplyResult.Success;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public ClubAcceptResult AcceptApplication(ulong clubOwnerUserId, string userName, out DiscordUser discordUser)
|
||||
{
|
||||
discordUser = null;
|
||||
@@ -188,7 +200,7 @@ public class ClubService : INService, IClubService
|
||||
uow.SaveChanges();
|
||||
return ClubAcceptResult.Accepted;
|
||||
}
|
||||
|
||||
|
||||
public ClubDenyResult RejectApplication(ulong clubOwnerUserId, string userName, out DiscordUser discordUser)
|
||||
{
|
||||
discordUser = null;
|
||||
@@ -201,9 +213,9 @@ public class ClubService : INService, IClubService
|
||||
club.Applicants.FirstOrDefault(x => x.User.ToString().ToUpperInvariant() == userName.ToUpperInvariant());
|
||||
if (applicant is null)
|
||||
return ClubDenyResult.NoSuchApplicant;
|
||||
|
||||
|
||||
club.Applicants.Remove(applicant);
|
||||
|
||||
|
||||
discordUser = applicant.User;
|
||||
uow.SaveChanges();
|
||||
return ClubDenyResult.Rejected;
|
||||
@@ -220,7 +232,7 @@ public class ClubService : INService, IClubService
|
||||
using var uow = _db.GetDbContext();
|
||||
var du = uow.GetOrCreateUser(user, x => x.Include(u => u.Club));
|
||||
if (du.Club is null)
|
||||
return ClubLeaveResult.NotInAClub;
|
||||
return ClubLeaveResult.NotInAClub;
|
||||
if (du.Club.OwnerId == du.Id)
|
||||
return ClubLeaveResult.OwnerCantLeave;
|
||||
|
||||
@@ -306,7 +318,7 @@ public class ClubService : INService, IClubService
|
||||
return ClubUnbanResult.Success;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public ClubKickResult Kick(ulong kickerId, string userName, out ClubInfo club)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
@@ -342,14 +354,14 @@ public class ClubService : INService, IClubService
|
||||
{
|
||||
if (!CheckClubName(clubName))
|
||||
return ClubRenameResult.NameTooLong;
|
||||
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
||||
|
||||
var club = uow.Set<ClubInfo>().GetByOwnerOrAdmin(userId);
|
||||
|
||||
|
||||
if (club is null)
|
||||
return ClubRenameResult.NotOwnerOrAdmin;
|
||||
|
||||
|
||||
if (await uow.Set<ClubInfo>().AnyAsyncEF(x => x.Name == clubName))
|
||||
return ClubRenameResult.NameTaken;
|
||||
|
||||
|
@@ -23,6 +23,7 @@ public interface IClubService
|
||||
ClubKickResult Kick(ulong kickerId, string userName, out ClubInfo club);
|
||||
List<ClubInfo> GetClubLeaderboardPage(int page);
|
||||
Task<ClubRenameResult> RenameClubAsync(ulong userId, string clubName);
|
||||
Task<int> GetClubRankAsync(int clubId);
|
||||
}
|
||||
|
||||
public enum ClubApplyResult
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>true</ImplicitUsings>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<Version>5.1.1</Version>
|
||||
<Version>5.1.5</Version>
|
||||
|
||||
<!-- Output/build -->
|
||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||
@@ -29,7 +29,7 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.6"/>
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1"/>
|
||||
<PackageReference Include="Discord.Net" Version="3.204.0"/>
|
||||
<PackageReference Include="Discord.Net" Version="3.15.3"/>
|
||||
<PackageReference Include="CoreCLR-NCalc" Version="3.1.246"/>
|
||||
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138"/>
|
||||
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414"/>
|
||||
@@ -70,7 +70,7 @@
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1"/>
|
||||
|
||||
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta17"/>
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.8"/>
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.9"/>
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14"/>
|
||||
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009"/>
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.0" />
|
||||
|
@@ -74,10 +74,4 @@ public sealed class ImageCache : IImageCache, INService
|
||||
|
||||
public Task<byte[]?> GetSlotBgAsync()
|
||||
=> GetImageDataAsync(_ic.Data.Slots.Bg);
|
||||
|
||||
public Task<byte[]?> GetRipBgAsync()
|
||||
=> GetImageDataAsync(_ic.Data.Rip.Bg);
|
||||
|
||||
public Task<byte[]?> GetRipOverlayAsync()
|
||||
=> GetImageDataAsync(_ic.Data.Rip.Overlay);
|
||||
}
|
@@ -147,4 +147,5 @@ public static class StringExtensions
|
||||
var newString = str.UnescapeUnicodeCodePoint();
|
||||
return newString;
|
||||
});
|
||||
|
||||
}
|
@@ -6,5 +6,5 @@ namespace NadekoBot.Modules.Permissions;
|
||||
[StructLayout(LayoutKind.Sequential, Size = 1)]
|
||||
public readonly struct CleverBotResponseStr
|
||||
{
|
||||
public const string CLEVERBOT_RESPONSE = "cleverbot:response";
|
||||
public const string CLEVERBOT_RESPONSE = "CLEVERBOT:RESPONSE";
|
||||
}
|
@@ -15,9 +15,11 @@ public sealed class DoAsUserMessage : IUserMessage
|
||||
_message = message;
|
||||
}
|
||||
|
||||
public ulong Id => _msg.Id;
|
||||
public ulong Id
|
||||
=> _msg.Id;
|
||||
|
||||
public DateTimeOffset CreatedAt => _msg.CreatedAt;
|
||||
public DateTimeOffset CreatedAt
|
||||
=> _msg.CreatedAt;
|
||||
|
||||
public Task DeleteAsync(RequestOptions? options = null)
|
||||
{
|
||||
@@ -56,67 +58,104 @@ public sealed class DoAsUserMessage : IUserMessage
|
||||
ReactionType type = ReactionType.Normal)
|
||||
=> _msg.GetReactionUsersAsync(emoji, limit, options, type);
|
||||
|
||||
public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emoji, int limit,
|
||||
public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(
|
||||
IEmote emoji,
|
||||
int limit,
|
||||
RequestOptions? options = null)
|
||||
{
|
||||
return _msg.GetReactionUsersAsync(emoji, limit, options);
|
||||
}
|
||||
|
||||
public MessageType Type => _msg.Type;
|
||||
public MessageType Type
|
||||
=> _msg.Type;
|
||||
|
||||
public MessageSource Source => _msg.Source;
|
||||
public MessageSource Source
|
||||
=> _msg.Source;
|
||||
|
||||
public bool IsTTS => _msg.IsTTS;
|
||||
public bool IsTTS
|
||||
=> _msg.IsTTS;
|
||||
|
||||
public bool IsPinned => _msg.IsPinned;
|
||||
public bool IsPinned
|
||||
=> _msg.IsPinned;
|
||||
|
||||
public bool IsSuppressed => _msg.IsSuppressed;
|
||||
public bool IsSuppressed
|
||||
=> _msg.IsSuppressed;
|
||||
|
||||
public bool MentionedEveryone => _msg.MentionedEveryone;
|
||||
public bool MentionedEveryone
|
||||
=> _msg.MentionedEveryone;
|
||||
|
||||
public string Content => _message;
|
||||
public string Content
|
||||
=> _message;
|
||||
|
||||
public string CleanContent => _msg.CleanContent;
|
||||
public string CleanContent
|
||||
=> _msg.CleanContent;
|
||||
|
||||
public DateTimeOffset Timestamp => _msg.Timestamp;
|
||||
public DateTimeOffset Timestamp
|
||||
=> _msg.Timestamp;
|
||||
|
||||
public DateTimeOffset? EditedTimestamp => _msg.EditedTimestamp;
|
||||
public DateTimeOffset? EditedTimestamp
|
||||
=> _msg.EditedTimestamp;
|
||||
|
||||
public IMessageChannel Channel => _msg.Channel;
|
||||
public IMessageChannel Channel
|
||||
=> _msg.Channel;
|
||||
|
||||
public IUser Author => _user;
|
||||
public IUser Author
|
||||
=> _user;
|
||||
|
||||
public IThreadChannel Thread => _msg.Thread;
|
||||
public IThreadChannel Thread
|
||||
=> _msg.Thread;
|
||||
|
||||
public IReadOnlyCollection<IAttachment> Attachments => _msg.Attachments;
|
||||
public IReadOnlyCollection<IAttachment> Attachments
|
||||
=> _msg.Attachments;
|
||||
|
||||
public IReadOnlyCollection<IEmbed> Embeds => _msg.Embeds;
|
||||
public IReadOnlyCollection<IEmbed> Embeds
|
||||
=> _msg.Embeds;
|
||||
|
||||
public IReadOnlyCollection<ITag> Tags => _msg.Tags;
|
||||
public IReadOnlyCollection<ITag> Tags
|
||||
=> _msg.Tags;
|
||||
|
||||
public IReadOnlyCollection<ulong> MentionedChannelIds => _msg.MentionedChannelIds;
|
||||
public IReadOnlyCollection<ulong> MentionedChannelIds
|
||||
=> _msg.MentionedChannelIds;
|
||||
|
||||
public IReadOnlyCollection<ulong> MentionedRoleIds => _msg.MentionedRoleIds;
|
||||
public IReadOnlyCollection<ulong> MentionedRoleIds
|
||||
=> _msg.MentionedRoleIds;
|
||||
|
||||
public IReadOnlyCollection<ulong> MentionedUserIds => _msg.MentionedUserIds;
|
||||
public IReadOnlyCollection<ulong> MentionedUserIds
|
||||
=> _msg.MentionedUserIds;
|
||||
|
||||
public MessageActivity Activity => _msg.Activity;
|
||||
public MessageActivity Activity
|
||||
=> _msg.Activity;
|
||||
|
||||
public MessageApplication Application => _msg.Application;
|
||||
public MessageApplication Application
|
||||
=> _msg.Application;
|
||||
|
||||
public MessageReference Reference => _msg.Reference;
|
||||
public MessageReference Reference
|
||||
=> _msg.Reference;
|
||||
|
||||
public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions => _msg.Reactions;
|
||||
public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions
|
||||
=> _msg.Reactions;
|
||||
|
||||
public IReadOnlyCollection<IMessageComponent> Components => _msg.Components;
|
||||
public IReadOnlyCollection<IMessageComponent> Components
|
||||
=> _msg.Components;
|
||||
|
||||
public IReadOnlyCollection<IStickerItem> Stickers => _msg.Stickers;
|
||||
public IReadOnlyCollection<IStickerItem> Stickers
|
||||
=> _msg.Stickers;
|
||||
|
||||
public MessageFlags? Flags => _msg.Flags;
|
||||
public MessageFlags? Flags
|
||||
=> _msg.Flags;
|
||||
|
||||
[Obsolete("Obsolete in favor of InteractionMetadata")]
|
||||
public IMessageInteraction Interaction => _msg.Interaction;
|
||||
public MessageRoleSubscriptionData RoleSubscriptionData => _msg.RoleSubscriptionData;
|
||||
public IMessageInteraction Interaction
|
||||
=> _msg.Interaction;
|
||||
|
||||
public MessageRoleSubscriptionData RoleSubscriptionData
|
||||
=> _msg.RoleSubscriptionData;
|
||||
|
||||
public PurchaseNotification PurchaseNotification
|
||||
=> _msg.PurchaseNotification;
|
||||
|
||||
public MessageCallData? CallData
|
||||
=> _msg.CallData;
|
||||
|
||||
public Task ModifyAsync(Action<MessageProperties> func, RequestOptions? options = null)
|
||||
{
|
||||
@@ -138,17 +177,39 @@ public sealed class DoAsUserMessage : IUserMessage
|
||||
return _msg.CrosspostAsync(options);
|
||||
}
|
||||
|
||||
public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name,
|
||||
public string Resolve(
|
||||
TagHandling userHandling = TagHandling.Name,
|
||||
TagHandling channelHandling = TagHandling.Name,
|
||||
TagHandling roleHandling = TagHandling.Name,
|
||||
TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name)
|
||||
TagHandling everyoneHandling = TagHandling.Ignore,
|
||||
TagHandling emojiHandling = TagHandling.Name)
|
||||
{
|
||||
return _msg.Resolve(userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling);
|
||||
}
|
||||
|
||||
public MessageResolvedData ResolvedData => _msg.ResolvedData;
|
||||
public Task EndPollAsync(RequestOptions options)
|
||||
=> _msg.EndPollAsync(options);
|
||||
|
||||
public IUserMessage ReferencedMessage => _msg.ReferencedMessage;
|
||||
public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetPollAnswerVotersAsync(
|
||||
uint answerId,
|
||||
int? limit = null,
|
||||
ulong? afterId = null,
|
||||
RequestOptions? options = null)
|
||||
=> _msg.GetPollAnswerVotersAsync(
|
||||
answerId,
|
||||
limit,
|
||||
afterId,
|
||||
options);
|
||||
|
||||
public MessageResolvedData ResolvedData
|
||||
=> _msg.ResolvedData;
|
||||
|
||||
public IUserMessage ReferencedMessage
|
||||
=> _msg.ReferencedMessage;
|
||||
|
||||
public IMessageInteractionMetadata InteractionMetadata
|
||||
=> _msg.InteractionMetadata;
|
||||
|
||||
public Poll? Poll
|
||||
=> _msg.Poll;
|
||||
}
|
@@ -8,7 +8,7 @@ namespace NadekoBot.Common;
|
||||
public partial class ImageUrls : ICloneable<ImageUrls>
|
||||
{
|
||||
[Comment("DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 3;
|
||||
public int Version { get; set; } = 4;
|
||||
|
||||
public CoinData Coins { get; set; }
|
||||
public Uri[] Currency { get; set; }
|
||||
@@ -16,16 +16,8 @@ public partial class ImageUrls : ICloneable<ImageUrls>
|
||||
public RategirlData Rategirl { get; set; }
|
||||
public XpData Xp { get; set; }
|
||||
|
||||
//new
|
||||
public RipData Rip { get; set; }
|
||||
public SlotData Slots { get; set; }
|
||||
|
||||
public class RipData
|
||||
{
|
||||
public Uri Bg { get; set; }
|
||||
public Uri Overlay { get; set; }
|
||||
}
|
||||
|
||||
public class SlotData
|
||||
{
|
||||
public Uri[] Emojis { get; set; }
|
||||
|
@@ -70,6 +70,7 @@ public abstract class NadekoInteractionBase
|
||||
{
|
||||
if (_singleUse)
|
||||
_interactionCompletedSource.TrySetResult(true);
|
||||
|
||||
await ExecuteOnActionAsync(smc);
|
||||
|
||||
if (!smc.HasResponded)
|
||||
|
@@ -12,11 +12,6 @@ public class FontProvider : INService
|
||||
public FontFamily NotoSans { get; }
|
||||
//public FontFamily Emojis { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Font used for .rip command
|
||||
/// </summary>
|
||||
public Font RipFont { get; }
|
||||
|
||||
public List<FontFamily> FallBackFonts { get; }
|
||||
private readonly FontCollection _fonts;
|
||||
|
||||
@@ -54,7 +49,6 @@ public class FontProvider : INService
|
||||
FallBackFonts.AddRange(_fonts.AddCollection(font));
|
||||
}
|
||||
|
||||
RipFont = NotoSans.CreateFont(20, FontStyle.Bold);
|
||||
DottyFont = FallBackFonts.First(x => x.Name == "dotty");
|
||||
}
|
||||
}
|
@@ -11,7 +11,5 @@ public interface IImageCache
|
||||
Task<byte[]?> GetDiceAsync(int num);
|
||||
Task<byte[]?> GetSlotEmojiAsync(int number);
|
||||
Task<byte[]?> GetSlotBgAsync();
|
||||
Task<byte[]?> GetRipBgAsync();
|
||||
Task<byte[]?> GetRipOverlayAsync();
|
||||
Task<byte[]?> GetImageDataAsync(Uri url);
|
||||
}
|
@@ -1,7 +1,30 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public static class NumberExtensions
|
||||
{
|
||||
public static DateTimeOffset ToUnixTimestamp(this double number)
|
||||
=> new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).AddSeconds(number);
|
||||
|
||||
public static string ToShortString(this decimal value)
|
||||
{
|
||||
if (value <= 1_000)
|
||||
return Math.Round(value, 2).ToString(CultureInfo.InvariantCulture);
|
||||
if (value <= 1_000_000)
|
||||
return Math.Round(value, 1).ToString(CultureInfo.InvariantCulture);
|
||||
var tokens = " MBtq";
|
||||
var i = 2;
|
||||
while (true)
|
||||
{
|
||||
var num = (decimal)Math.Pow(1000, i);
|
||||
if (num > value)
|
||||
{
|
||||
var num2 = (decimal)Math.Pow(1000, i - 1);
|
||||
return $"{Math.Round((value / num2), 1)}{tokens[i - 1]}".Trim();
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
@@ -30,6 +30,8 @@ greettest:
|
||||
- greettest
|
||||
greetdmtest:
|
||||
- greetdmtest
|
||||
boosttest:
|
||||
- boosttest
|
||||
byetest:
|
||||
- byetest
|
||||
boost:
|
||||
@@ -945,6 +947,10 @@ warnexpire:
|
||||
warnclear:
|
||||
- warnclear
|
||||
- warnc
|
||||
warndelete:
|
||||
- warndelete
|
||||
- warnrm
|
||||
- warnd
|
||||
warnpunishlist:
|
||||
- warnpunishlist
|
||||
- warnpl
|
||||
@@ -1144,8 +1150,6 @@ discordpermoverridereset:
|
||||
- dpor
|
||||
rafflecur:
|
||||
- rafflecur
|
||||
rip:
|
||||
- rip
|
||||
timelyset:
|
||||
- timelyset
|
||||
timely:
|
||||
@@ -1192,6 +1196,8 @@ xpreset:
|
||||
- xpreset
|
||||
bible:
|
||||
- bible
|
||||
quran:
|
||||
- quran
|
||||
edit:
|
||||
- edit
|
||||
delete:
|
||||
@@ -1402,4 +1408,12 @@ cleanupguilddata:
|
||||
prompt:
|
||||
- prompt
|
||||
honeypot:
|
||||
- honeypot
|
||||
- honeypot
|
||||
coins:
|
||||
- coins
|
||||
- crypto
|
||||
- cryptos
|
||||
afk:
|
||||
- afk
|
||||
keep:
|
||||
- keep
|
@@ -1,5 +1,5 @@
|
||||
# DO NOT CHANGE
|
||||
version: 4
|
||||
version: 5
|
||||
# Hangman related settings (.hangman command)
|
||||
hangman:
|
||||
# The amount of currency awarded to the winner of a hangman game
|
||||
@@ -56,19 +56,27 @@ raceAnimals:
|
||||
name: Unicorn
|
||||
# Which chatbot API should bot use.
|
||||
# 'cleverbot' - bot will use Cleverbot API.
|
||||
# 'gpt' - bot will use GPT API
|
||||
chatBot: Gpt
|
||||
# 'openai' - bot will use OpenAi API
|
||||
chatBot: OpenAi
|
||||
chatGpt:
|
||||
# Url to any openai api compatible url.
|
||||
# Make sure to modify the modelName appropriately
|
||||
# DO NOT add /v1/chat/completions suffix to the url
|
||||
apiUrl: https://api.openai.com
|
||||
# Which GPT Model should bot use.
|
||||
# gpt35turbo - cheapest
|
||||
# gpt4o - more expensive, higher quality
|
||||
# gpt-3.5-turbo - cheapest
|
||||
# gpt-4o - more expensive, higher quality
|
||||
#
|
||||
modelName: Gpt35Turbo
|
||||
# How should the chat bot behave, what's its personality? (Usage of this counts towards the max tokens)
|
||||
# If you are using another openai compatible api, you may use any of the models supported by that api
|
||||
modelName: gpt-3.5-turbo
|
||||
# How should the chatbot behave, what's its personality?
|
||||
# This will be sent as a system message.
|
||||
# Usage of this counts towards the max tokens.
|
||||
personalityPrompt: You are a chat bot willing to have a conversation with anyone about anything.
|
||||
# The maximum number of messages in a conversation that can be remembered. (This will increase the number of tokens used)
|
||||
# The maximum number of messages in a conversation that can be remembered.
|
||||
# This will increase the number of tokens used.
|
||||
chatHistory: 5
|
||||
# The maximum number of tokens to use per GPT API call
|
||||
# The maximum number of tokens to use per OpenAi API call
|
||||
maxTokens: 100
|
||||
# The minimum number of tokens to use per GPT API call, such that chat history is removed to make room.
|
||||
minTokens: 30
|
||||
|
@@ -112,6 +112,14 @@ greettest:
|
||||
params:
|
||||
- user:
|
||||
desc: "The user to impersonate when sending the greeting, defaulting to yourself if not specified."
|
||||
boosttest:
|
||||
desc: Sends the boost message in the current channel as if you just boosted the server. You can optionally specify a different user.
|
||||
ex:
|
||||
- ''
|
||||
- '@SomeoneElse'
|
||||
params:
|
||||
- user:
|
||||
desc: "The user to impersonate when sending the boost message, defaulting to yourself if not specified."
|
||||
greetdmtest:
|
||||
desc: Sends the greet direct message to you as if you just joined the server. You can optionally specify a different user.
|
||||
ex:
|
||||
@@ -568,7 +576,7 @@ deleterole:
|
||||
- Awesome Role
|
||||
params:
|
||||
- role:
|
||||
desc: "The role being deleted, as identified by its unique identifier."
|
||||
desc: "The role being deleted, as identified by its id."
|
||||
rolecolor:
|
||||
desc: Set a role's color using its hex value. Provide no color in order to see the hex value of the color of the specified role. The role you specify has to be lower in the role hierarchy than your highest role.
|
||||
ex:
|
||||
@@ -597,11 +605,11 @@ ban:
|
||||
- time:
|
||||
desc: "The duration of the temporary ban."
|
||||
userId:
|
||||
desc: "The unique identifier of the user being banned."
|
||||
desc: "The id of the user being banned."
|
||||
msg:
|
||||
desc: "The reason for the ban is provided in this message."
|
||||
- userId:
|
||||
desc: "The unique identifier of the user being banned."
|
||||
desc: "The id of the user being banned."
|
||||
msg:
|
||||
desc: "The reason for the ban is provided in this message."
|
||||
- user:
|
||||
@@ -619,7 +627,7 @@ softban:
|
||||
msg:
|
||||
desc: "The reason for the ban is described in this string."
|
||||
- userId:
|
||||
desc: "The unique identifier for the user being banned and then unbanned."
|
||||
desc: "The id of the user being banned and then unbanned."
|
||||
msg:
|
||||
desc: "The reason for the ban is described in this string."
|
||||
kick:
|
||||
@@ -835,7 +843,12 @@ setservericon:
|
||||
- img:
|
||||
desc: "The URL of the image file to be displayed as the bot's banner."
|
||||
send:
|
||||
desc: 'Sends a message to a channel or user. Channel or user can be '
|
||||
desc: |-
|
||||
Sends a message to a channel or user.
|
||||
You can write "channel" (literally word 'channel') first followed by the channel id or channel mention, or
|
||||
You can write "user" (literally word 'user') first followed by the user id or user mention.
|
||||
After either one of those, specify the message to be sent.
|
||||
This command can only be used by the Bot Owner.
|
||||
ex:
|
||||
- channel 123123123132312 Stop spamming commands plz
|
||||
- user 1231231232132 I can see in the console what you're doing.
|
||||
@@ -1190,7 +1203,7 @@ userblacklist:
|
||||
- action:
|
||||
desc: "The type of operation to perform on the user, either adding or removing them from the blacklist."
|
||||
id:
|
||||
desc: "The unique identifier of the user to be added, removed, or listed."
|
||||
desc: "The id of the user to be added, removed, or listed."
|
||||
- action:
|
||||
desc: "The type of operation to perform on the user, either adding or removing them from the blacklist."
|
||||
usr:
|
||||
@@ -1210,7 +1223,7 @@ channelblacklist:
|
||||
- action:
|
||||
desc: "The type of operation to perform on the channel, either adding it to the blacklist or removing it from it."
|
||||
id:
|
||||
desc: "The unique identifier of the channel being added, removed, or listed."
|
||||
desc: "The id of the channel being added, removed, or listed."
|
||||
serverblacklist:
|
||||
desc: |-
|
||||
Either [add]s or [rem]oves a server, or servers specified by an ID from a blacklist.
|
||||
@@ -1226,7 +1239,7 @@ serverblacklist:
|
||||
- action:
|
||||
desc: "The type of operation to perform on the server(s). It can be either adding or removing them from the blacklist."
|
||||
id:
|
||||
desc: "The unique identifier of the server being added, removed, or listed."
|
||||
desc: "The id of the server being added, removed, or listed."
|
||||
- action:
|
||||
desc: "The type of operation to perform on the server(s). It can be either adding or removing them from the blacklist."
|
||||
guild:
|
||||
@@ -1283,7 +1296,7 @@ quoteshow:
|
||||
- 123
|
||||
params:
|
||||
- quoteId:
|
||||
desc: "The unique identifier for the quote being queried."
|
||||
desc: "The id of the quote being queried."
|
||||
quotesearch:
|
||||
desc: 'Shows a random quote given a search query. Partially matches in several ways: 1) Only content of any quote, 2) only by author, 3) keyword and content, 3) or keyword and author'
|
||||
ex:
|
||||
@@ -1304,14 +1317,14 @@ quoteid:
|
||||
- 123456
|
||||
params:
|
||||
- quoteId:
|
||||
desc: "The unique identifier for the quote to be displayed."
|
||||
desc: "The id of the quote to be displayed."
|
||||
quotedelete:
|
||||
desc: Deletes a quote with the specified ID. You have to either have the Manage Messages permission or be the creator of the quote to delete it.
|
||||
ex:
|
||||
- 123456
|
||||
params:
|
||||
- quoteId:
|
||||
desc: "The unique identifier for the quote being deleted."
|
||||
desc: "The id of the quote being deleted."
|
||||
quotedeleteauthor:
|
||||
desc: Deletes all quotes by the specified author. If the author is not you, then ManageMessage server permission is required.
|
||||
ex:
|
||||
@@ -1353,10 +1366,11 @@ flip:
|
||||
desc: "The number of times the coin is flipped."
|
||||
betflip:
|
||||
desc: |-
|
||||
Bet to guess will the result be heads or tails.
|
||||
Guessing awards you 1.95x the currency you've bet (rounded up).
|
||||
Bet on the coin flip.
|
||||
The result can be heads or tails.
|
||||
Guessing correctly rewards you with 1.95x of the currency you've bet (rounded up).
|
||||
Multiplier can be changed by the bot owner.
|
||||
|
||||
You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance.
|
||||
ex:
|
||||
- 5 heads
|
||||
- 3 t
|
||||
@@ -1505,7 +1519,7 @@ betroll:
|
||||
desc: |-
|
||||
Bets the specified amount of currency and rolls a dice.
|
||||
Rolling over 66 yields x2 of your currency, over 90 - x4 and 100 x10.
|
||||
You can type 'all', 'half' or 'X%' to bet that part of your current balance.
|
||||
You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance.
|
||||
ex:
|
||||
- 5
|
||||
params:
|
||||
@@ -1516,7 +1530,7 @@ luckyladder:
|
||||
Bets the specified amount of currency on the lucky ladder.
|
||||
You can stop on one of many different multipliers.
|
||||
The won amount is rounded down to the nearest whole number.
|
||||
You can type 'all', 'half' or 'X%' to bet that part of your current balance.
|
||||
You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance.
|
||||
ex:
|
||||
- 10
|
||||
params:
|
||||
@@ -1621,8 +1635,8 @@ choose:
|
||||
rps:
|
||||
desc: |-
|
||||
Play a game of Rocket-Paperclip-Scissors with Nadeko.
|
||||
You can bet on it. Multiplier is the same as on betflip.
|
||||
You can type 'all', 'half' or 'X%' to bet that part of your current balance.
|
||||
You can bet on it. Winning awards you 1.95x of the bet.
|
||||
You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance.
|
||||
ex:
|
||||
- r 100
|
||||
- scissors
|
||||
@@ -1808,7 +1822,7 @@ load:
|
||||
- 5
|
||||
params:
|
||||
- id:
|
||||
desc: "The unique identifier of the playlist to be loaded."
|
||||
desc: "The id of the playlist to be loaded."
|
||||
playlists:
|
||||
desc: Lists all playlists. Paginated, 20 per page.
|
||||
ex:
|
||||
@@ -1822,7 +1836,7 @@ playlistshow:
|
||||
- 1
|
||||
params:
|
||||
- id:
|
||||
desc: "The unique identifier for the playlist to retrieve songs from."
|
||||
desc: "The id of the playlist to retrieve songs from."
|
||||
page:
|
||||
desc: "The current page number for the pagination."
|
||||
deleteplaylist:
|
||||
@@ -2064,11 +2078,11 @@ image:
|
||||
- query:
|
||||
desc: "The search term used to retrieve the desired image."
|
||||
lmgtfy:
|
||||
desc: Google something for an idiot.
|
||||
desc: Google something for a baka.
|
||||
ex:
|
||||
- query
|
||||
- How to eat a banana
|
||||
params:
|
||||
- ffs:
|
||||
- smh:
|
||||
desc: "The search query to be entered into the search engine."
|
||||
google:
|
||||
desc: Get a Google search link for some terms.
|
||||
@@ -2218,7 +2232,7 @@ currencytransaction:
|
||||
- 3yvd
|
||||
params:
|
||||
- id:
|
||||
desc: "The unique identifier for the transaction being queried."
|
||||
desc: "The id of the transaction being queried."
|
||||
listperms:
|
||||
desc: Lists whole permission chain with their indexes. You can specify an optional page number if there are a lot of permissions.
|
||||
ex:
|
||||
@@ -2289,7 +2303,11 @@ emojiremove:
|
||||
- emotes:
|
||||
desc: "The list of emojis to be removed from the server."
|
||||
stickeradd:
|
||||
desc: Adds the sticker from your message to this server. Send the sticker along with this command (in the same message).
|
||||
desc: |-
|
||||
Adds the sticker from your message to this server.
|
||||
Send the sticker along with this command (in the same message).
|
||||
Alternatively you can upload an image along with this command but you have to specify the name.
|
||||
The image must be 300x300 in .png or .apng format and up to 512KB in size.
|
||||
ex:
|
||||
- ''
|
||||
- name "description" tag1 tag2 tagN
|
||||
@@ -2700,9 +2718,10 @@ betstats:
|
||||
slot:
|
||||
desc: |-
|
||||
Play Nadeko slots by placing your bet.
|
||||
You can type 'all', 'half' or 'X%' to bet that part of your current balance.
|
||||
You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance.
|
||||
ex:
|
||||
- 5
|
||||
- 'all'
|
||||
params:
|
||||
- amount:
|
||||
desc: "The amount of currency to bet."
|
||||
@@ -2743,9 +2762,11 @@ waifutransfer:
|
||||
newOwner:
|
||||
desc: "The user to whom ownership of the waifu is being transferred."
|
||||
waifugift:
|
||||
desc: -|
|
||||
Gift an item to someone.
|
||||
This will increase their waifu value by a percentage of the gift's value.
|
||||
desc: |-
|
||||
Gift an item to a waifu user.
|
||||
The waifu's value will be increased by the percentage of the gift's value.
|
||||
You can optionally prefix the gift with a multiplier to gift the item that many times.
|
||||
For example, 3xRose will give the waifu 3 roses, 10xBread will give the waifu 10 breads. Do not use plural forms.
|
||||
Negative gifts will not show up in waifuinfo.
|
||||
Provide no parameters to see a list of items that you can gift.
|
||||
ex:
|
||||
@@ -2753,9 +2774,9 @@ waifugift:
|
||||
- Rose @Himesama
|
||||
params:
|
||||
- page:
|
||||
desc: "The number of pages to display when listing available gifting options."
|
||||
- itemName:
|
||||
desc: "The name of an item to be gifted, which is used to determine the percentage increase in waifu value."
|
||||
desc: "The number of the page to display."
|
||||
- items:
|
||||
desc: "The name of an item to be gifted. With an optional multiplier prefix."
|
||||
waifu:
|
||||
desc: "The user who is receiving the gift."
|
||||
waifulb:
|
||||
@@ -3157,6 +3178,13 @@ warnclear:
|
||||
desc: "The ID of the user whose warnings are being cleared."
|
||||
index:
|
||||
desc: "The index of the warning to be cleared, or 0 to clear all warnings."
|
||||
warndelete:
|
||||
desc: Deletes a warning from a user by its index.
|
||||
ex:
|
||||
- 3
|
||||
params:
|
||||
- index:
|
||||
desc: "The index of the warning to be deleted."
|
||||
warnpunishlist:
|
||||
desc: Lists punishments for warnings.
|
||||
ex:
|
||||
@@ -3747,11 +3775,15 @@ expredit:
|
||||
- 123 I'm a magical girl
|
||||
params:
|
||||
- id:
|
||||
desc: "The unique identifier for the expression being edited."
|
||||
desc: "The id of the expression being edited."
|
||||
message:
|
||||
desc: "The text that will replace the original response in the expression's output."
|
||||
say:
|
||||
desc: Bot will send the message you typed in the specified channel. If you omit the channel name, it will send the message in the current channel. Supports embeds.
|
||||
desc: |-
|
||||
Make the bot say something, or in other words, make the bot send the message.
|
||||
You can optionally specify the channel where the bot will send the message.
|
||||
If you omit the channel name, it will send the message in the current channel.
|
||||
Supports embeds.
|
||||
ex:
|
||||
- hi
|
||||
- '#chat hi'
|
||||
@@ -3853,13 +3885,6 @@ rafflecur:
|
||||
desc: "The minimum or maximum amount of currency that can be used for betting."
|
||||
mixed:
|
||||
desc: "The parameter determines whether the raffle operates in \"fixed\" or \"proportional\" mode."
|
||||
rip:
|
||||
desc: Shows the inevitable fate of someone.
|
||||
ex:
|
||||
- '@Someone'
|
||||
params:
|
||||
- usr:
|
||||
desc: "The user whose fate is being revealed."
|
||||
autodisconnect:
|
||||
desc: Toggles whether the bot should disconnect from the voice channel once it's done playing all of the songs and queue repeat option is set to `none`.
|
||||
ex:
|
||||
@@ -4056,7 +4081,7 @@ xpshopbuy:
|
||||
- type:
|
||||
desc: "The type of item to purchase, such as a skill or a cosmetic."
|
||||
key:
|
||||
desc: "The unique identifier for the item being purchased."
|
||||
desc: "The id of the item being purchased."
|
||||
xpshopuse:
|
||||
desc: Use a previously purchased item from the xp shop by specifying the type and the key of the item.
|
||||
ex:
|
||||
@@ -4066,7 +4091,7 @@ xpshopuse:
|
||||
- type:
|
||||
desc: "The type of item to be used, such as an experience point or a skill upgrade."
|
||||
key:
|
||||
desc: "The unique identifier for the item in the XP shop that you want to use."
|
||||
desc: "The id of the item in the XP shop that you want to use."
|
||||
bible:
|
||||
desc: Shows bible verse. You need to supply book name and chapter:verse
|
||||
ex:
|
||||
@@ -4076,6 +4101,16 @@ bible:
|
||||
desc: "The name of the biblical book being referenced."
|
||||
chapterAndVerse:
|
||||
desc: "The reference to a specific passage in the Bible, such as 'Genesis 3:15'"
|
||||
quran:
|
||||
desc: |-
|
||||
Shows the text of an ayah of the Quran, as well as the recitation by Alafasy.
|
||||
Supply surah:ayah, or ayah number. For instance, 262 or 2:255 will both get you Ayat Al Kursi
|
||||
ex:
|
||||
- 2:255
|
||||
- 262
|
||||
params:
|
||||
- ayah:
|
||||
desc: "The number of the ayah in the Quran, for example 2:255."
|
||||
edit:
|
||||
desc: Edits bot's message, you have to specify message ID and new text. You can optionally specify target channel. Supports embeds.
|
||||
ex:
|
||||
@@ -4084,13 +4119,13 @@ edit:
|
||||
- '#other-channel 771562360594628608 {{"description":"hello"}}'
|
||||
params:
|
||||
- messageId:
|
||||
desc: "The unique identifier of the message being edited."
|
||||
desc: "The id of the message being edited."
|
||||
text:
|
||||
desc: "The new text content of the edited message."
|
||||
- channel:
|
||||
desc: "The target channel where the edited message will be sent or updated in."
|
||||
messageId:
|
||||
desc: "The unique identifier of the message being edited."
|
||||
desc: "The id of the message being edited."
|
||||
text:
|
||||
desc: "The new text content of the edited message."
|
||||
delete:
|
||||
@@ -4101,13 +4136,13 @@ delete:
|
||||
- 771562360594628608 5m
|
||||
params:
|
||||
- messageId:
|
||||
desc: "The unique identifier of a specific message within a channel, used to target the deletion operation."
|
||||
desc: "The id of a specific message within a channel, used to target the deletion operation."
|
||||
time:
|
||||
desc: "The duration after which the message should be automatically deleted."
|
||||
- channel:
|
||||
desc: "The channel where the message is located or should be searched for."
|
||||
messageId:
|
||||
desc: "The unique identifier of a specific message within a channel, used to target the deletion operation."
|
||||
desc: "The id of a specific message within a channel, used to target the deletion operation."
|
||||
time:
|
||||
desc: "The duration after which the message should be automatically deleted."
|
||||
roleid:
|
||||
@@ -4242,8 +4277,10 @@ bankbalance:
|
||||
Shows how much currency is in your bank account.
|
||||
This differs from your cash amount, as the cash amount is publicly available, but only you have access to your bank balance.
|
||||
However, you have to withdraw it first in order to use it.
|
||||
Bot Owner can also check another user's bank balance.
|
||||
ex:
|
||||
- ''
|
||||
- '@User'
|
||||
params:
|
||||
- {}
|
||||
banktake:
|
||||
@@ -4258,7 +4295,7 @@ banktake:
|
||||
- amount:
|
||||
desc: "The total value of funds being withdrawn."
|
||||
userId:
|
||||
desc: "The unique identifier for the user whose account is being accessed."
|
||||
desc: "The id of the user whose account is being accessed."
|
||||
bankaward:
|
||||
desc: Award the specified amount of currency to a user's bank
|
||||
ex:
|
||||
@@ -4308,6 +4345,7 @@ betdraw:
|
||||
You can specify `r` or `b` for red or black, and `h` or `l` for high or low.
|
||||
You can specify only h/l or only r/b or both.
|
||||
Returns are high but **7 always loses**.
|
||||
You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance.
|
||||
ex:
|
||||
- 50 r
|
||||
- 200 b l
|
||||
@@ -4448,7 +4486,7 @@ todoedit:
|
||||
- abc This is an updated entry
|
||||
params:
|
||||
- todoId:
|
||||
desc: "The unique identifier for the todo item being edited."
|
||||
desc: "The id of the todo item being edited."
|
||||
newMessage:
|
||||
desc: "The text of a new task description or update to an existing one."
|
||||
todocomplete:
|
||||
@@ -4457,14 +4495,14 @@ todocomplete:
|
||||
- 4a
|
||||
params:
|
||||
- todoId:
|
||||
desc: "The unique identifier for the todo item being marked as completed."
|
||||
desc: "The id of the todo item being marked as completed."
|
||||
tododelete:
|
||||
desc: Deletes a todo with the specified ID.
|
||||
ex:
|
||||
- abc
|
||||
params:
|
||||
- todoId:
|
||||
desc: "The unique identifier for the todo item being deleted."
|
||||
desc: "The id of the todo item being deleted."
|
||||
todoclear:
|
||||
desc: Deletes all unarchived todos.
|
||||
ex:
|
||||
@@ -4498,7 +4536,7 @@ todoshow:
|
||||
- 4a
|
||||
params:
|
||||
- todoId:
|
||||
desc: "The unique identifier for the todo item being displayed."
|
||||
desc: "The id of the todo item being displayed."
|
||||
todoarchivedelete:
|
||||
desc: Deletes the archived todo list with the specified ID.
|
||||
ex:
|
||||
@@ -4530,5 +4568,34 @@ honeypot:
|
||||
This is useful for automatically getting rid of spam bots.
|
||||
ex:
|
||||
- ''
|
||||
params:
|
||||
- {}
|
||||
coins:
|
||||
desc: |-
|
||||
Shows a list of 10 crypto currencies ordered by market cap.
|
||||
Shows their price, change in the last24h, market cap and circulating and total supply.
|
||||
Paginated with 10 per page.
|
||||
ex:
|
||||
- ''
|
||||
- '2'
|
||||
params:
|
||||
- page:
|
||||
desc: "Page number to show. Starts at 1."
|
||||
afk:
|
||||
desc: |-
|
||||
Toggles AFK status for yourself with the specified message.
|
||||
If you don't provide a message it default to a generic one.
|
||||
Anyone @ mentioning you in any server will receive the afk message.
|
||||
This will only work if the other user's message starts with the mention.
|
||||
ex:
|
||||
- ''
|
||||
params:
|
||||
- msg:
|
||||
desc: "The message to send when someone pings you."
|
||||
keep:
|
||||
desc: |-
|
||||
The current serve, won't be deleted from Nadeko's database during the purge.
|
||||
ex:
|
||||
- ''
|
||||
params:
|
||||
- {}
|
@@ -38,6 +38,7 @@
|
||||
"prune_cancelled": "Pruning was cancelled.",
|
||||
"prune_not_found": "No active prune was found on this server.",
|
||||
"prune_progress": "Pruning... {0}/{1} messages deleted.",
|
||||
"prune_already_running": "A prune is already running on this server.",
|
||||
"timeoutdm": "You have been timed out in {0} server.\nReason: {1}",
|
||||
"timedout_user": "User Timed Out",
|
||||
"remove_roles_pl": "have had their roles removed",
|
||||
@@ -701,6 +702,7 @@
|
||||
"warn_count": "{0} current, {1} total",
|
||||
"warnlog_for": "Warnlog for {0}",
|
||||
"warnpl_none": "No punishments set.",
|
||||
"warning_not_found": "Warning not found.",
|
||||
"warn_expire_set_delete": "Warnings will be deleted after {0} days.",
|
||||
"warn_expire_set_clear": "Warnings will be cleared after {0} days.",
|
||||
"warn_expire_reset": "Warnings will no longer expire.",
|
||||
@@ -711,6 +713,7 @@
|
||||
"warn_punish_rem": "Having {0} warnings will no longer trigger a punishment.",
|
||||
"warn_punish_set": "I will apply {0} punishment to users with {1} warnings.",
|
||||
"warn_punish_set_timed": "I will apply {0} punishment for {2} to users with {1} warnings.",
|
||||
"warning_deleted": "Warning {0} has been deleted.",
|
||||
"time_new": "Time",
|
||||
"timezone": "Timezone",
|
||||
"timezone_db_api_key": "You need to activate your TimezoneDB API key. You can do so by clicking on the link you've received in the email with your API key.",
|
||||
@@ -889,6 +892,7 @@
|
||||
"club_kick_hierarchy": "Only club owner can kick club admins. Owner can't be kicked.",
|
||||
"club_renamed": "Club has been renamed to {0}",
|
||||
"club_name_taken": "A club with that name already exists.",
|
||||
"rank": "Rank",
|
||||
"template_reloaded": "Xp template has been reloaded.",
|
||||
"expr_edited": "Expression Edited",
|
||||
"self_assign_are_exclusive": "You can only choose 1 role from each group.",
|
||||
@@ -1039,6 +1043,7 @@
|
||||
"medusa_already_loaded": "Medusa {0} is already loaded",
|
||||
"medusa_invalid_not_found": "Medusa with that name wasn't found or the file was invalid",
|
||||
"bank_balance": "You have {0} in your bank account.",
|
||||
"bank_balance_other": "User {0} has {1} in the bank.",
|
||||
"bank_deposited": "You deposited {0} to your bank account.",
|
||||
"bank_withdrew": "You withdrew {0} from your bank account.",
|
||||
"bank_withdraw_insuff": "You don't have sufficient {0} in your bank account.",
|
||||
@@ -1067,8 +1072,7 @@
|
||||
"xpshop_already_owned": "You already own this item.",
|
||||
"xpshop_item_not_found": "An item with that key doesn't exist.",
|
||||
"xpshop_website": "You can see the list of all Xp Shop items here: <https://xpshop.nadeko.bot>",
|
||||
"sticker_invalid_size": "Stickers must be exactly 300x300 pixels.",
|
||||
"sticker_error": "You must either send a sticker along with this command, or upload a 300x300 .png or .apng image.",
|
||||
"sticker_error": "You must either send a sticker along with this command, or upload a 300x300 .png or .apng image. Up to 512KB in size.",
|
||||
"sticker_missing_name": "Please specify a name for the sticker.",
|
||||
"thread_deleted": "Thread Deleted",
|
||||
"thread_created": "Thread Created",
|
||||
@@ -1103,5 +1107,7 @@
|
||||
"queue_search_results": "Type the number of the search result to queue up that track.",
|
||||
"overloads": "Overloads",
|
||||
"honeypot_on": "Honeypot enabled on this channel." ,
|
||||
"honeypot_off": "Honeypot disabled."
|
||||
"honeypot_off": "Honeypot disabled.",
|
||||
"afk_set": "AFK message set. Type a message in any channel to clear.",
|
||||
"rero_message_not_found": "The specified message wasn't found. Make sure you've specified the message from this channel."
|
||||
}
|
||||
|
Reference in New Issue
Block a user