mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 01:38:27 -04:00
Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f68f219a25 | ||
|
8f16b11d02 | ||
|
df5eced904 | ||
|
1dcd158f43 | ||
|
757c9b564d | ||
|
07cef3eb5e | ||
|
85c525e19b | ||
|
477581f616 | ||
|
ff30105816 | ||
|
49f04a594b | ||
|
716090a132 | ||
|
c835514c7b | ||
|
b136e7ff0e | ||
|
9dd2997b0f | ||
|
fde5309ea4 | ||
|
a8e4173e9b | ||
|
74b4c4b64d | ||
|
6cc5a160a2 | ||
|
ca8e022db6 | ||
|
cd8c14c607 | ||
|
1340533c21 | ||
|
14d86b9042 | ||
|
3a504a954f | ||
|
822ce0b8de | ||
|
40490a4656 | ||
|
0cf7909fef | ||
|
de8d4b7d9e | ||
|
0123892038 | ||
|
d00e59567a | ||
|
0aba2fdcaf | ||
|
bb910a8188 | ||
|
bdad9cc17a | ||
|
5d76a15dc0 | ||
|
a7be56a562 | ||
|
3c108e531e | ||
|
c473669cbc | ||
|
b97c486b80 | ||
|
716e092fd0 | ||
|
a362ee90fc |
107
CHANGELOG.md
107
CHANGELOG.md
@@ -2,6 +2,113 @@
|
|||||||
|
|
||||||
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
||||||
|
|
||||||
|
## [5.1.16] - 28.10.2024
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Added .ncanvas and related commands.
|
||||||
|
- You can set pixel colors (and text) on a 500x350 canvas, pepega version of r/place
|
||||||
|
- You use currency to set pixels.
|
||||||
|
- Commands:
|
||||||
|
- see the entire canvas: `.nc`
|
||||||
|
- zoom: `.ncz <pos>` or `.ncz x y`
|
||||||
|
- set pixel: `.ncsp <pos> <color> <text?>`
|
||||||
|
- get pixel: `.ncp <pos>`
|
||||||
|
- Owners can use .ncsetimg to set a starting image, use `.h .setimg` for instructions
|
||||||
|
- Owners can reset the whole canvas via `.ncreset`
|
||||||
|
|
||||||
|
## [5.1.15] - 21.10.2024
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Added -c option for `.xpglb`
|
||||||
|
-
|
||||||
|
|
||||||
|
## Change
|
||||||
|
|
||||||
|
- Leaderboards will now show 10 users per page
|
||||||
|
- A lot of internal changes and improvements
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- Fixed a big issue which caused several features to not get loaded on bot restart
|
||||||
|
- Alias collision fix `.qse` is now quotesearch, `.qs` will stay `.queuesearch`
|
||||||
|
- Fixed some migrations which would prevent users from updating from ancient versions
|
||||||
|
- Waifulb will no longer show #0000 discrims
|
||||||
|
- More `.greet` command fixes
|
||||||
|
- Author name will now be counted as content in embeds. Embeds can now only have author fields and still be valid
|
||||||
|
- Grpc api fixes, and additions
|
||||||
|
|
||||||
|
## [5.1.14] - 03.10.2024
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- Improved `.xplb -c`, it will now correctly only show users who are still in the server with no count limit
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- Fixed medusa load error on startup
|
||||||
|
|
||||||
|
## [5.1.13] - 03.10.2024
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Grpc api server will no longer start unless enabled in creds
|
||||||
|
- Seq comment in creds fixed
|
||||||
|
|
||||||
|
## [5.1.12] - 03.10.2024
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for `seq` for logging. If you fill in seq url and apiKey in creds.yml, bot will sends logs to it
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed another bug in `.greet` / `.bye` system, which caused it to show wrong message on a wrong server occasionally
|
||||||
|
|
||||||
|
## [5.1.11] - 03.10.2024
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `%user.displayname%` placeholder. It will show users nickname, if there is one, otherwise it will show the username.
|
||||||
|
- Nickname won't be shown in bye messages.
|
||||||
|
- Added initial version of grpc api. Beta
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed a bug which caused `.bye` and `.greet` messages to be randomly disabled
|
||||||
|
- Fixed `.lb -c` breaking sometimes, and fixed pagination
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Youtube now always uses `yt-dlp`. Dropped support for `youtube-dl`
|
||||||
|
- If you've previously renamed your yt-dlp file to youtube-dl, please rename it back.
|
||||||
|
- ytProvider in data/searches.yml now also controls where you're getting your song streams from.
|
||||||
|
- (Invidious support added for .q)
|
||||||
|
|
||||||
|
## [5.1.10] - 24.09.2024
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed claimed waifu decay in `games.yml`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Added some logs for greet service in case there are unforeseen issues, for easier debugging
|
||||||
|
|
||||||
|
## [5.1.9] - 21.09.2024
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `.greettest`, and other `.*test` commands if you didn't have them enabled.
|
||||||
|
- Fixed `.greetdmtest` sending messages twice.
|
||||||
|
- Fixed a serious bug which caused greet messages to be jumbled up, and wrong ones to be sent for the wrong events.
|
||||||
|
- There is no database issue, all greet messages are safe, the cache was caching any setting every 3 seconds with no regard for the type of the event
|
||||||
|
- This also caused `.greetdm` messages to not be sent if `.greet` is enabled
|
||||||
|
- This bug was introduced in 5.1.8. PLEASE UPDATE if you are on 5.1.8
|
||||||
|
- Selfhosters only: Fixed medusa dependency loading
|
||||||
|
- Note: Make sure to not publish any other DLLs besides the ones you are sure you will need, as there can be version conflicts which didn't happen before.
|
||||||
|
|
||||||
## [5.1.8] - 19.09.2024
|
## [5.1.8] - 19.09.2024
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@@ -30,6 +30,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nadeko.Medusa", "src\Nadeko
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Generators", "src\NadekoBot.Generators\NadekoBot.Generators.csproj", "{92770AF3-83EE-49F1-A0BB-79124D19A13D}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Generators", "src\NadekoBot.Generators\NadekoBot.Generators.csproj", "{92770AF3-83EE-49F1-A0BB-79124D19A13D}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.GrpcApiBase", "src\NadekoBot.GrpcApiBase\NadekoBot.GrpcApiBase.csproj", "{FB74B9EA-10B9-4542-ACB1-35523A95A587}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -79,6 +81,12 @@ Global
|
|||||||
{92770AF3-83EE-49F1-A0BB-79124D19A13D}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
|
{92770AF3-83EE-49F1-A0BB-79124D19A13D}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{92770AF3-83EE-49F1-A0BB-79124D19A13D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{92770AF3-83EE-49F1-A0BB-79124D19A13D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{92770AF3-83EE-49F1-A0BB-79124D19A13D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{92770AF3-83EE-49F1-A0BB-79124D19A13D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{FB74B9EA-10B9-4542-ACB1-35523A95A587}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{FB74B9EA-10B9-4542-ACB1-35523A95A587}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{FB74B9EA-10B9-4542-ACB1-35523A95A587}.GlobalNadeko|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{FB74B9EA-10B9-4542-ACB1-35523A95A587}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{FB74B9EA-10B9-4542-ACB1-35523A95A587}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{FB74B9EA-10B9-4542-ACB1-35523A95A587}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -91,6 +99,7 @@ Global
|
|||||||
{E685977E-31A4-46F4-A5D7-4E3E39E82E43} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
{E685977E-31A4-46F4-A5D7-4E3E39E82E43} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||||
{92770AF3-83EE-49F1-A0BB-79124D19A13D} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
{92770AF3-83EE-49F1-A0BB-79124D19A13D} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||||
{2F4CF6D6-0C2F-4944-B204-9508CDA53195} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
{2F4CF6D6-0C2F-4944-B204-9508CDA53195} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||||
|
{FB74B9EA-10B9-4542-ACB1-35523A95A587} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {5F3F555C-855F-4BE8-B526-D062D3E8ACA4}
|
SolutionGuid = {5F3F555C-855F-4BE8-B526-D062D3E8ACA4}
|
||||||
|
184
src/NadekoBot.Generators/GrpcApiPermGenerator.cs
Normal file
184
src/NadekoBot.Generators/GrpcApiPermGenerator.cs
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System.CodeDom.Compiler;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace NadekoBot.Generators
|
||||||
|
{
|
||||||
|
public readonly record struct MethodPermData
|
||||||
|
{
|
||||||
|
public readonly ImmutableArray<(string Name, string Value)> MethodPerms;
|
||||||
|
public readonly ImmutableArray<string> NoAuthRequired;
|
||||||
|
|
||||||
|
public MethodPermData(ImmutableArray<(string Name, string Value)> methodPerms,
|
||||||
|
ImmutableArray<string> noAuthRequired)
|
||||||
|
{
|
||||||
|
MethodPerms = methodPerms;
|
||||||
|
NoAuthRequired = noAuthRequired;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Generator]
|
||||||
|
public class GrpcApiPermGenerator : IIncrementalGenerator
|
||||||
|
{
|
||||||
|
public const string GRPC_API_PERM_ATTRIBUTE =
|
||||||
|
"""
|
||||||
|
namespace NadekoBot.GrpcApi;
|
||||||
|
|
||||||
|
[System.AttributeUsage(System.AttributeTargets.Method)]
|
||||||
|
public class GrpcApiPermAttribute : System.Attribute
|
||||||
|
{
|
||||||
|
public GuildPerm Value { get; }
|
||||||
|
public GrpcApiPermAttribute(GuildPerm value) => Value = value;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
public const string GRPC_NO_AUTH_REQUIRED_ATTRIBUTE =
|
||||||
|
"""
|
||||||
|
namespace NadekoBot.GrpcApi;
|
||||||
|
|
||||||
|
[System.AttributeUsage(System.AttributeTargets.Method)]
|
||||||
|
public class GrpcNoAuthRequiredAttribute : System.Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
|
{
|
||||||
|
context.RegisterPostInitializationOutput(ctx => ctx.AddSource("GrpcApiPermAttribute.cs",
|
||||||
|
SourceText.From(GRPC_API_PERM_ATTRIBUTE, Encoding.UTF8)));
|
||||||
|
|
||||||
|
context.RegisterPostInitializationOutput(ctx => ctx.AddSource("GrpcNoAuthRequiredAttribute.cs",
|
||||||
|
SourceText.From(GRPC_NO_AUTH_REQUIRED_ATTRIBUTE, Encoding.UTF8)));
|
||||||
|
|
||||||
|
var perms = context.SyntaxProvider
|
||||||
|
.ForAttributeWithMetadataName(
|
||||||
|
"NadekoBot.GrpcApi.GrpcApiPermAttribute",
|
||||||
|
predicate: static (s, _) => s is MethodDeclarationSyntax,
|
||||||
|
transform: static (ctx, _) => GetMethodSemanticTargets(ctx.SemanticModel, ctx.TargetNode))
|
||||||
|
.Where(static m => m is not null)
|
||||||
|
.Select(static (x, _) => x!.Value)
|
||||||
|
.Collect();
|
||||||
|
|
||||||
|
|
||||||
|
var all = context.SyntaxProvider
|
||||||
|
.ForAttributeWithMetadataName(
|
||||||
|
"NadekoBot.GrpcApi.GrpcNoAuthRequiredAttribute",
|
||||||
|
predicate: static (s, _) => s is MethodDeclarationSyntax,
|
||||||
|
transform: static (ctx, _) => GetNoAuthMethodName(ctx.SemanticModel, ctx.TargetNode))
|
||||||
|
.Collect()
|
||||||
|
.Combine(perms)
|
||||||
|
.Select((x, _) => new MethodPermData(x.Right, x.Left));
|
||||||
|
|
||||||
|
context.RegisterSourceOutput(all,
|
||||||
|
static (spc, source) => Execute(source, spc));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetNoAuthMethodName(SemanticModel model, SyntaxNode node)
|
||||||
|
=> ((MethodDeclarationSyntax)node).Identifier.Text;
|
||||||
|
|
||||||
|
private static (string Name, string Value)? GetMethodSemanticTargets(SemanticModel model, SyntaxNode node)
|
||||||
|
{
|
||||||
|
var method = (MethodDeclarationSyntax)node;
|
||||||
|
|
||||||
|
var name = method.Identifier.Text;
|
||||||
|
var attr = method.AttributeLists
|
||||||
|
.SelectMany(x => x.Attributes)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (attr is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (name, attr.ArgumentList?.Arguments[0].ToString() ?? "__missing_perm__");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Execute(MethodPermData data, SourceProductionContext ctx)
|
||||||
|
{
|
||||||
|
using (var stringWriter = new StringWriter())
|
||||||
|
using (var sw = new IndentedTextWriter(stringWriter))
|
||||||
|
{
|
||||||
|
sw.WriteLine("using System.Collections.Frozen;");
|
||||||
|
sw.WriteLine();
|
||||||
|
sw.WriteLine("namespace NadekoBot.GrpcApi;");
|
||||||
|
sw.WriteLine();
|
||||||
|
|
||||||
|
sw.WriteLine("public partial class GrpcApiPermsInterceptor");
|
||||||
|
sw.WriteLine("{");
|
||||||
|
|
||||||
|
sw.Indent++;
|
||||||
|
|
||||||
|
sw.WriteLine(
|
||||||
|
"private static FrozenDictionary<string, GuildPerm> _perms = new Dictionary<string, GuildPerm>()");
|
||||||
|
sw.WriteLine("{");
|
||||||
|
|
||||||
|
sw.Indent++;
|
||||||
|
foreach (var field in data.MethodPerms)
|
||||||
|
{
|
||||||
|
sw.WriteLine("{{ \"{0}\", {1} }},", field.Name, field.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.Indent--;
|
||||||
|
sw.WriteLine("}.ToFrozenDictionary();");
|
||||||
|
|
||||||
|
sw.WriteLine();
|
||||||
|
sw.WriteLine("private static FrozenSet<string> _noAuthRequired = new HashSet<string>()");
|
||||||
|
sw.WriteLine("{");
|
||||||
|
|
||||||
|
sw.Indent++;
|
||||||
|
foreach (var noauth in data.NoAuthRequired)
|
||||||
|
{
|
||||||
|
sw.WriteLine("{{ \"{0}\" }},", noauth);
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.WriteLine("");
|
||||||
|
|
||||||
|
sw.Indent--;
|
||||||
|
sw.WriteLine("}.ToFrozenSet();");
|
||||||
|
|
||||||
|
sw.Indent--;
|
||||||
|
sw.WriteLine("}");
|
||||||
|
|
||||||
|
sw.Flush();
|
||||||
|
ctx.AddSource("GrpcApiInterceptor.g.cs", stringWriter.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TranslationPair> GetFields(string? dataText)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(dataText))
|
||||||
|
return new();
|
||||||
|
|
||||||
|
Dictionary<string, string> data;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var output = JsonConvert.DeserializeObject<Dictionary<string, string>>(dataText!);
|
||||||
|
if (output is null)
|
||||||
|
return new();
|
||||||
|
|
||||||
|
data = output;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Debug.WriteLine("Failed parsing responses file.");
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = new List<TranslationPair>();
|
||||||
|
foreach (var entry in data)
|
||||||
|
{
|
||||||
|
list.Add(new(
|
||||||
|
entry.Key,
|
||||||
|
entry.Value
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -9,7 +9,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" PrivateAssets="all" GeneratePathProperty="true" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" PrivateAssets="all" GeneratePathProperty="true" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
21
src/NadekoBot.GrpcApiBase/NadekoBot.GrpcApiBase.csproj
Normal file
21
src/NadekoBot.GrpcApiBase/NadekoBot.GrpcApiBase.csproj
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Google.Protobuf" Version="3.28.2" />
|
||||||
|
<PackageReference Include="Grpc" Version="2.46.6" />
|
||||||
|
<PackageReference Include="Grpc.Tools" Version="2.66.0" PrivateAssets="All" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Protobuf Include="protos/*.proto">
|
||||||
|
<GrpcServices>Server</GrpcServices>
|
||||||
|
</Protobuf>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
47
src/NadekoBot.GrpcApiBase/protos/canvas.proto
Normal file
47
src/NadekoBot.GrpcApiBase/protos/canvas.proto
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option csharp_namespace = "NadekoBot.GrpcApi";
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
|
package ncanvas;
|
||||||
|
|
||||||
|
service GrpcNCanvas {
|
||||||
|
rpc GetCanvas(google.protobuf.Empty) returns (CanvasReply);
|
||||||
|
rpc GetPixel(GetPixelRequest) returns (GetPixelReply);
|
||||||
|
rpc SetPixel(SetPixelRequest) returns (SetPixelReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
message CanvasReply {
|
||||||
|
repeated uint32 pixels = 1;
|
||||||
|
int32 width = 2;
|
||||||
|
int32 height = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPixelRequest {
|
||||||
|
int32 x = 1;
|
||||||
|
int32 y = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetPixelReply {
|
||||||
|
string color = 1;
|
||||||
|
uint32 packedColor = 2;
|
||||||
|
int32 positionX = 3;
|
||||||
|
int32 positionY = 4;
|
||||||
|
int64 price = 5;
|
||||||
|
string text = 6;
|
||||||
|
string position = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetPixelRequest {
|
||||||
|
string position = 1;
|
||||||
|
string color = 2;
|
||||||
|
string text = 3;
|
||||||
|
int64 price = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetPixelReply {
|
||||||
|
string error = 1;
|
||||||
|
bool success = 2;
|
||||||
|
optional GetPixelReply pixel = 3;
|
||||||
|
}
|
89
src/NadekoBot.GrpcApiBase/protos/exprs.proto
Normal file
89
src/NadekoBot.GrpcApiBase/protos/exprs.proto
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option csharp_namespace = "NadekoBot.GrpcApi";
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
|
package exprs;
|
||||||
|
|
||||||
|
service GrpcExprs {
|
||||||
|
rpc GetExprs(GetExprsRequest) returns (GetExprsReply);
|
||||||
|
rpc AddExpr(AddExprRequest) returns (AddExprReply);
|
||||||
|
rpc DeleteExpr(DeleteExprRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
|
rpc GetQuotes(GetQuotesRequest) returns (GetQuotesReply);
|
||||||
|
rpc AddQuote(AddQuoteRequest) returns (AddQuoteReply);
|
||||||
|
rpc DeleteQuote(DeleteQuoteRequest) returns (google.protobuf.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteExprRequest {
|
||||||
|
string id = 1;
|
||||||
|
uint64 guildId = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetExprsRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
string query = 2;
|
||||||
|
int32 page = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetExprsReply {
|
||||||
|
repeated ExprDto expressions = 1;
|
||||||
|
int32 totalCount = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ExprDto {
|
||||||
|
string id = 1;
|
||||||
|
string trigger = 2;
|
||||||
|
string response = 3;
|
||||||
|
|
||||||
|
bool ca = 4;
|
||||||
|
bool ad = 5;
|
||||||
|
bool dm = 6;
|
||||||
|
bool at = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddExprRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
ExprDto expr = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddExprReply {
|
||||||
|
string id = 1;
|
||||||
|
bool success = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetQuotesRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
string query = 2;
|
||||||
|
int32 page = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetQuotesReply {
|
||||||
|
repeated QuoteDto quotes = 1;
|
||||||
|
int32 totalCount = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message QuoteDto {
|
||||||
|
string id = 1;
|
||||||
|
string trigger = 2;
|
||||||
|
string response = 3;
|
||||||
|
|
||||||
|
uint64 authorId = 4;
|
||||||
|
string authorName = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddQuoteRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
QuoteDto quote = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddQuoteReply {
|
||||||
|
string id = 1;
|
||||||
|
bool success = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteQuoteRequest {
|
||||||
|
string id = 1;
|
||||||
|
uint64 guildId = 2;
|
||||||
|
}
|
60
src/NadekoBot.GrpcApiBase/protos/fin.proto
Normal file
60
src/NadekoBot.GrpcApiBase/protos/fin.proto
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option csharp_namespace = "NadekoBot.GrpcApi";
|
||||||
|
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
package fin;
|
||||||
|
|
||||||
|
service GrpcFin {
|
||||||
|
rpc GetTransactions(GetTransactionsRequest) returns (GetTransactionsReply);
|
||||||
|
rpc GetHoldings(GetHoldingsRequest) returns (GetHoldingsReply);
|
||||||
|
rpc Withdraw(WithdrawRequest) returns (WithdrawReply);
|
||||||
|
rpc Deposit(DepositRequest) returns (DepositReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetTransactionsRequest {
|
||||||
|
int32 page = 1;
|
||||||
|
uint64 userId = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetTransactionsReply {
|
||||||
|
repeated TransactionReply transactions = 1;
|
||||||
|
int32 total = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TransactionReply {
|
||||||
|
int64 amount = 1;
|
||||||
|
string note = 2;
|
||||||
|
string type = 3;
|
||||||
|
string extra = 4;
|
||||||
|
google.protobuf.Timestamp timestamp = 5;
|
||||||
|
string id = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetHoldingsRequest {
|
||||||
|
uint64 userId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetHoldingsReply {
|
||||||
|
int64 cash = 1;
|
||||||
|
int64 bank = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WithdrawRequest {
|
||||||
|
uint64 userId = 1;
|
||||||
|
int64 amount = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WithdrawReply {
|
||||||
|
bool success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DepositRequest {
|
||||||
|
uint64 userId = 1;
|
||||||
|
int64 amount = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DepositReply {
|
||||||
|
bool success = 1;
|
||||||
|
}
|
51
src/NadekoBot.GrpcApiBase/protos/greet.proto
Normal file
51
src/NadekoBot.GrpcApiBase/protos/greet.proto
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option csharp_namespace = "NadekoBot.GrpcApi";
|
||||||
|
|
||||||
|
package greet;
|
||||||
|
|
||||||
|
service GrpcGreet {
|
||||||
|
rpc GetGreetSettings (GetGreetRequest) returns (GrpcGreetSettings);
|
||||||
|
rpc UpdateGreet (UpdateGreetRequest) returns (UpdateGreetReply);
|
||||||
|
rpc TestGreet (TestGreetRequest) returns (TestGreetReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
message GrpcGreetSettings {
|
||||||
|
string channelId = 1;
|
||||||
|
string message = 2;
|
||||||
|
bool isEnabled = 3;
|
||||||
|
GrpcGreetType type = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetGreetRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
GrpcGreetType type = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateGreetRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
GrpcGreetSettings settings = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GrpcGreetType {
|
||||||
|
Greet = 0;
|
||||||
|
GreetDm = 1;
|
||||||
|
Bye = 2;
|
||||||
|
Boost = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateGreetReply {
|
||||||
|
bool Success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TestGreetRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
uint64 channelId = 2;
|
||||||
|
uint64 userId = 3;
|
||||||
|
GrpcGreetType type = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TestGreetReply {
|
||||||
|
bool success = 1;
|
||||||
|
string error = 2;
|
||||||
|
}
|
144
src/NadekoBot.GrpcApiBase/protos/other.proto
Normal file
144
src/NadekoBot.GrpcApiBase/protos/other.proto
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option csharp_namespace = "NadekoBot.GrpcApi";
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
|
package other;
|
||||||
|
|
||||||
|
service GrpcOther {
|
||||||
|
rpc BotOnGuild(BotOnGuildRequest) returns (BotOnGuildReply);
|
||||||
|
rpc GetTextChannels(GetTextChannelsRequest) returns (GetTextChannelsReply);
|
||||||
|
rpc GetRoles(GetRolesRequest) returns (GetRolesReply);
|
||||||
|
|
||||||
|
rpc GetCurrencyLb(GetLbRequest) returns (CurrencyLbReply);
|
||||||
|
rpc GetXpLb(GetLbRequest) returns (XpLbReply);
|
||||||
|
rpc GetWaifuLb(GetLbRequest) returns (WaifuLbReply);
|
||||||
|
|
||||||
|
rpc GetShardStats(google.protobuf.Empty) returns (stream ShardStatsReply);
|
||||||
|
rpc GetCommandFeed(google.protobuf.Empty) returns (stream CommandFeedEntry);
|
||||||
|
rpc GetServerInfo(ServerInfoRequest) returns (GetServerInfoReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
message CommandFeedEntry {
|
||||||
|
string command = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetRolesRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetRolesReply {
|
||||||
|
repeated RoleReply roles = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BotOnGuildRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BotOnGuildReply {
|
||||||
|
bool success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ShardStatsReply {
|
||||||
|
int32 id = 1;
|
||||||
|
string status = 2;
|
||||||
|
|
||||||
|
int32 guildCount = 3;
|
||||||
|
string uptime = 4;
|
||||||
|
int64 commands = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetTextChannelsRequest{
|
||||||
|
uint64 guildId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetTextChannelsReply {
|
||||||
|
repeated TextChannelReply textChannels = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TextChannelReply {
|
||||||
|
uint64 id = 1;
|
||||||
|
string name = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CurrencyLbReply {
|
||||||
|
repeated CurrencyLbEntryReply entries = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CurrencyLbEntryReply {
|
||||||
|
string user = 1;
|
||||||
|
uint64 userId = 2;
|
||||||
|
int64 amount = 3;
|
||||||
|
string avatar = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetLbRequest {
|
||||||
|
int32 page = 1;
|
||||||
|
int32 perPage = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message XpLbReply {
|
||||||
|
repeated XpLbEntryReply entries = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message XpLbEntryReply {
|
||||||
|
string user = 1;
|
||||||
|
uint64 userId = 2;
|
||||||
|
int64 totalXp = 3;
|
||||||
|
int64 level = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WaifuLbReply {
|
||||||
|
repeated WaifuLbEntry entries = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WaifuLbEntry {
|
||||||
|
string user = 1;
|
||||||
|
string claimedBy = 2;
|
||||||
|
int64 value = 3;
|
||||||
|
bool isMutual = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ServerInfoRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetServerInfoReply {
|
||||||
|
uint64 id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string iconUrl = 3;
|
||||||
|
uint64 ownerId = 4;
|
||||||
|
string ownerName = 5;
|
||||||
|
repeated RoleReply roles = 6;
|
||||||
|
repeated EmojiReply emojis = 7;
|
||||||
|
repeated string features = 8;
|
||||||
|
int32 textChannels = 9;
|
||||||
|
int32 voiceChannels = 10;
|
||||||
|
int32 memberCount = 11;
|
||||||
|
int64 createdAt = 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RoleReply {
|
||||||
|
uint64 id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string iconUrl = 3;
|
||||||
|
string color = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message EmojiReply {
|
||||||
|
string name = 1;
|
||||||
|
string url = 2;
|
||||||
|
string code = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChannelReply {
|
||||||
|
uint64 id = 1;
|
||||||
|
string name = 2;
|
||||||
|
ChannelType type = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ChannelType {
|
||||||
|
Text = 0;
|
||||||
|
Voice = 1;
|
||||||
|
}
|
107
src/NadekoBot.GrpcApiBase/protos/warn.proto
Normal file
107
src/NadekoBot.GrpcApiBase/protos/warn.proto
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option csharp_namespace = "NadekoBot.GrpcApi";
|
||||||
|
|
||||||
|
package warn;
|
||||||
|
|
||||||
|
service GrpcWarn {
|
||||||
|
rpc GetWarnSettings (WarnSettingsRequest) returns (WarnSettingsReply);
|
||||||
|
|
||||||
|
rpc SetWarnExpiry(SetWarnExpiryRequest) returns (SetWarnExpiryReply);
|
||||||
|
rpc AddWarnp (AddWarnpRequest) returns (AddWarnpReply);
|
||||||
|
rpc DeleteWarnp (DeleteWarnpRequest) returns (DeleteWarnpReply);
|
||||||
|
|
||||||
|
rpc GetLatestWarnings(GetLatestWarningsRequest) returns (GetLatestWarningsReply);
|
||||||
|
rpc GetUserWarnings(GetUserWarningsRequest) returns (GetUserWarningsReply);
|
||||||
|
|
||||||
|
rpc ForgiveWarning(ForgiveWarningRequest) returns (ForgiveWarningReply);
|
||||||
|
rpc DeleteWarning(ForgiveWarningRequest) returns (ForgiveWarningReply);
|
||||||
|
|
||||||
|
}
|
||||||
|
message WarnSettingsRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WarnPunishment {
|
||||||
|
int32 threshold = 1;
|
||||||
|
string action = 2;
|
||||||
|
int32 duration = 3;
|
||||||
|
string role = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WarnSettingsReply {
|
||||||
|
repeated WarnPunishment punishments = 1;
|
||||||
|
int32 expiryDays = 2;
|
||||||
|
bool deleteOnExpire = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddWarnpRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
WarnPunishment punishment = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AddWarnpReply {
|
||||||
|
bool success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteWarnpRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
int32 threshold = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteWarnpReply {
|
||||||
|
bool success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserWarningsRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
string user = 2;
|
||||||
|
int32 page = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserWarningsReply {
|
||||||
|
repeated Warning warnings = 1;
|
||||||
|
int32 totalCount = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Warning {
|
||||||
|
string id = 1;
|
||||||
|
string reason = 2;
|
||||||
|
int64 timestamp = 3;
|
||||||
|
int64 weight = 4;
|
||||||
|
bool forgiven = 5;
|
||||||
|
string forgivenBy = 6;
|
||||||
|
string user = 7;
|
||||||
|
uint64 userId = 8;
|
||||||
|
string moderator = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ForgiveWarningRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
string warnId = 2;
|
||||||
|
string modName = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ForgiveWarningReply {
|
||||||
|
bool success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetWarnExpiryRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
int32 expiryDays = 2;
|
||||||
|
bool deleteOnExpire = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetWarnExpiryReply {
|
||||||
|
bool success = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetLatestWarningsRequest {
|
||||||
|
uint64 guildId = 1;
|
||||||
|
int32 page = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetLatestWarningsReply {
|
||||||
|
repeated Warning warnings = 1;
|
||||||
|
int32 totalCount = 2;
|
||||||
|
}
|
@@ -25,7 +25,7 @@ public sealed class Bot : IBot
|
|||||||
public bool IsReady { get; private set; }
|
public bool IsReady { get; private set; }
|
||||||
public int ShardId { get; set; }
|
public int ShardId { get; set; }
|
||||||
|
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
private readonly CommandService _commandService;
|
private readonly CommandService _commandService;
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
|
|
||||||
@@ -42,6 +42,9 @@ public sealed class Bot : IBot
|
|||||||
_credsProvider = new BotCredsProvider(totalShards, credPath);
|
_credsProvider = new BotCredsProvider(totalShards, credPath);
|
||||||
_creds = _credsProvider.GetCreds();
|
_creds = _credsProvider.GetCreds();
|
||||||
|
|
||||||
|
LogSetup.SetupLogger(shardId, _creds);
|
||||||
|
Log.Information("Pid: {ProcessId}", Environment.ProcessId);
|
||||||
|
|
||||||
_db = new NadekoDbService(_credsProvider);
|
_db = new NadekoDbService(_credsProvider);
|
||||||
|
|
||||||
var messageCacheSize =
|
var messageCacheSize =
|
||||||
@@ -115,7 +118,7 @@ public sealed class Bot : IBot
|
|||||||
// svcs.Components.Remove<IPlanner, Planner>();
|
// svcs.Components.Remove<IPlanner, Planner>();
|
||||||
// svcs.Components.Add<IPlanner, RemovablePlanner>();
|
// svcs.Components.Add<IPlanner, RemovablePlanner>();
|
||||||
|
|
||||||
svcs.AddSingleton<IBotCredentials>(_ => _credsProvider.GetCreds());
|
svcs.AddSingleton<IBotCreds>(_ => _credsProvider.GetCreds());
|
||||||
svcs.AddSingleton<DbService, DbService>(_db);
|
svcs.AddSingleton<DbService, DbService>(_db);
|
||||||
svcs.AddSingleton<IBotCredsProvider>(_credsProvider);
|
svcs.AddSingleton<IBotCredsProvider>(_credsProvider);
|
||||||
svcs.AddSingleton<DiscordSocketClient>(Client);
|
svcs.AddSingleton<DiscordSocketClient>(Client);
|
||||||
|
@@ -87,14 +87,7 @@ public static class DiscordUserExtensions
|
|||||||
> users.AsQueryable().Where(y => y.UserId == id).Select(y => y.TotalXp).FirstOrDefault())
|
> users.AsQueryable().Where(y => y.UserId == id).Select(y => y.TotalXp).FirstOrDefault())
|
||||||
.Count()
|
.Count()
|
||||||
+ 1;
|
+ 1;
|
||||||
|
|
||||||
public static async Task<IReadOnlyCollection<DiscordUser>> GetUsersXpLeaderboardFor(this DbSet<DiscordUser> users, int page, int perPage)
|
|
||||||
=> await users.ToLinqToDBTable()
|
|
||||||
.OrderByDescending(x => x.TotalXp)
|
|
||||||
.Skip(page * perPage)
|
|
||||||
.Take(perPage)
|
|
||||||
.ToArrayAsyncLinqToDB();
|
|
||||||
|
|
||||||
public static Task<List<DiscordUser>> GetTopRichest(
|
public static Task<List<DiscordUser>> GetTopRichest(
|
||||||
this DbSet<DiscordUser> users,
|
this DbSet<DiscordUser> users,
|
||||||
ulong botId,
|
ulong botId,
|
||||||
|
@@ -57,8 +57,7 @@ public static class GuildConfigExtensions
|
|||||||
List<ulong> availableGuilds)
|
List<ulong> availableGuilds)
|
||||||
{
|
{
|
||||||
var result = await configs
|
var result = await configs
|
||||||
.AsQueryable()
|
.IncludeEverything()
|
||||||
.Include(x => x.CommandCooldowns)
|
|
||||||
.Where(x => availableGuilds.Contains(x.GuildId))
|
.Where(x => availableGuilds.Contains(x.GuildId))
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
@@ -96,7 +95,6 @@ public static class GuildConfigExtensions
|
|||||||
GuildId = guildId,
|
GuildId = guildId,
|
||||||
Permissions = Permissionv2.GetDefaultPermlist,
|
Permissions = Permissionv2.GetDefaultPermlist,
|
||||||
WarningsInitialized = true,
|
WarningsInitialized = true,
|
||||||
WarnPunishments = DefaultWarnPunishments
|
|
||||||
});
|
});
|
||||||
ctx.SaveChanges();
|
ctx.SaveChanges();
|
||||||
}
|
}
|
||||||
@@ -104,7 +102,6 @@ public static class GuildConfigExtensions
|
|||||||
if (!config.WarningsInitialized)
|
if (!config.WarningsInitialized)
|
||||||
{
|
{
|
||||||
config.WarningsInitialized = true;
|
config.WarningsInitialized = true;
|
||||||
config.WarnPunishments = DefaultWarnPunishments;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
|
@@ -26,17 +26,6 @@ public static class UserXpExtensions
|
|||||||
return usr;
|
return usr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<IReadOnlyCollection<UserXpStats>> GetUsersFor(
|
|
||||||
this DbSet<UserXpStats> xps,
|
|
||||||
ulong guildId,
|
|
||||||
int page)
|
|
||||||
=> await xps.ToLinqToDBTable()
|
|
||||||
.Where(x => x.GuildId == guildId)
|
|
||||||
.OrderByDescending(x => x.Xp + x.AwardedXp)
|
|
||||||
.Skip(page * 9)
|
|
||||||
.Take(9)
|
|
||||||
.ToArrayAsyncLinqToDB();
|
|
||||||
|
|
||||||
public static async Task<List<UserXpStats>> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count)
|
public static async Task<List<UserXpStats>> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count)
|
||||||
=> await xps.ToLinqToDBTable()
|
=> await xps.ToLinqToDBTable()
|
||||||
.Where(x => x.GuildId == guildId)
|
.Where(x => x.GuildId == guildId)
|
||||||
|
@@ -77,7 +77,6 @@ public class GuildConfig : DbEntity
|
|||||||
public HashSet<UnroleTimer> UnroleTimer { get; set; } = new();
|
public HashSet<UnroleTimer> UnroleTimer { get; set; } = new();
|
||||||
public HashSet<VcRoleInfo> VcRoleInfos { get; set; }
|
public HashSet<VcRoleInfo> VcRoleInfos { get; set; }
|
||||||
public HashSet<CommandAlias> CommandAliases { get; set; } = new();
|
public HashSet<CommandAlias> CommandAliases { get; set; } = new();
|
||||||
public List<WarningPunishment> WarnPunishments { get; set; } = new();
|
|
||||||
public bool WarningsInitialized { get; set; }
|
public bool WarningsInitialized { get; set; }
|
||||||
public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { get; set; }
|
public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { get; set; }
|
||||||
public HashSet<SlowmodeIgnoredRole> SlowmodeIgnoredRoles { get; set; }
|
public HashSet<SlowmodeIgnoredRole> SlowmodeIgnoredRoles { get; set; }
|
||||||
|
19
src/NadekoBot/Db/Models/NCanvas/NCanvas.cs
Normal file
19
src/NadekoBot/Db/Models/NCanvas/NCanvas.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace NadekoBot.Db.Models;
|
||||||
|
|
||||||
|
public class NCPixel
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public required int Position { get; init; }
|
||||||
|
|
||||||
|
public required long Price { get; init; }
|
||||||
|
|
||||||
|
public required ulong OwnerId { get; init; }
|
||||||
|
public required uint Color { get; init; }
|
||||||
|
|
||||||
|
[MaxLength(256)]
|
||||||
|
public required string Text { get; init; }
|
||||||
|
}
|
@@ -3,6 +3,7 @@ namespace NadekoBot.Db.Models;
|
|||||||
|
|
||||||
public class WarningPunishment : DbEntity
|
public class WarningPunishment : DbEntity
|
||||||
{
|
{
|
||||||
|
public ulong GuildId { get; set; }
|
||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
public PunishmentAction Punishment { get; set; }
|
public PunishmentAction Punishment { get; set; }
|
||||||
public int Time { get; set; }
|
public int Time { get; set; }
|
||||||
|
@@ -62,7 +62,6 @@ public abstract class NadekoContext : DbContext
|
|||||||
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
|
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
|
||||||
public DbSet<HoneypotChannel> HoneyPotChannels { get; set; }
|
public DbSet<HoneypotChannel> HoneyPotChannels { get; set; }
|
||||||
|
|
||||||
// todo add guild colors
|
|
||||||
// public DbSet<GuildColors> GuildColors { get; set; }
|
// public DbSet<GuildColors> GuildColors { get; set; }
|
||||||
|
|
||||||
|
|
||||||
@@ -74,6 +73,16 @@ public abstract class NadekoContext : DbContext
|
|||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
#region NCanvas
|
||||||
|
|
||||||
|
modelBuilder.Entity<NCPixel>()
|
||||||
|
.HasAlternateKey(x => x.Position);
|
||||||
|
|
||||||
|
modelBuilder.Entity<NCPixel>()
|
||||||
|
.HasIndex(x => x.OwnerId);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region QUOTES
|
#region QUOTES
|
||||||
|
|
||||||
var quoteEntity = modelBuilder.Entity<Quote>();
|
var quoteEntity = modelBuilder.Entity<Quote>();
|
||||||
@@ -195,11 +204,6 @@ public abstract class NadekoContext : DbContext
|
|||||||
.WithOne()
|
.WithOne()
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
modelBuilder.Entity<GuildConfig>()
|
|
||||||
.HasMany(x => x.WarnPunishments)
|
|
||||||
.WithOne()
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
|
|
||||||
modelBuilder.Entity<GuildConfig>()
|
modelBuilder.Entity<GuildConfig>()
|
||||||
.HasMany(x => x.SlowmodeIgnoredRoles)
|
.HasMany(x => x.SlowmodeIgnoredRoles)
|
||||||
.WithOne()
|
.WithOne()
|
||||||
@@ -277,6 +281,18 @@ public abstract class NadekoContext : DbContext
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region WarningPunishments
|
||||||
|
|
||||||
|
var warnpunishmentEntity = modelBuilder.Entity<WarningPunishment>(b =>
|
||||||
|
{
|
||||||
|
b.HasAlternateKey(x => new
|
||||||
|
{
|
||||||
|
x.GuildId,
|
||||||
|
x.Count
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Self Assignable Roles
|
#region Self Assignable Roles
|
||||||
|
|
||||||
@@ -339,6 +355,7 @@ public abstract class NadekoContext : DbContext
|
|||||||
du.HasIndex(x => x.TotalXp);
|
du.HasIndex(x => x.TotalXp);
|
||||||
du.HasIndex(x => x.CurrencyAmount);
|
du.HasIndex(x => x.CurrencyAmount);
|
||||||
du.HasIndex(x => x.UserId);
|
du.HasIndex(x => x.UserId);
|
||||||
|
du.HasIndex(x => x.Username);
|
||||||
});
|
});
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -695,7 +712,7 @@ public abstract class NadekoContext : DbContext
|
|||||||
gs
|
gs
|
||||||
.Property(x => x.IsEnabled)
|
.Property(x => x.IsEnabled)
|
||||||
.HasDefaultValue(false);
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
gs
|
gs
|
||||||
.Property(x => x.AutoDeleteTimer)
|
.Property(x => x.AutoDeleteTimer)
|
||||||
.HasDefaultValue(0);
|
.HasDefaultValue(0);
|
||||||
|
@@ -38,6 +38,7 @@ left join guildconfigs on reactionrolemessage.guildconfigid = guildconfigs.id;")
|
|||||||
DELETE FROM "DelMsgOnCmdChannel" WHERE "GuildConfigId" is NULL;
|
DELETE FROM "DelMsgOnCmdChannel" WHERE "GuildConfigId" is NULL;
|
||||||
DELETE FROM "WarningPunishment" WHERE "GuildConfigId" NOT IN (SELECT "Id" from "GuildConfigs");
|
DELETE FROM "WarningPunishment" WHERE "GuildConfigId" NOT IN (SELECT "Id" from "GuildConfigs");
|
||||||
DELETE FROM "StreamRoleBlacklistedUser" WHERE "StreamRoleSettingsId" is NULL;
|
DELETE FROM "StreamRoleBlacklistedUser" WHERE "StreamRoleSettingsId" is NULL;
|
||||||
|
DELETE FROM "Permissions" WHERE "GuildConfigId" NOT IN (SELECT "Id" from "GuildConfigs");
|
||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,4 +66,20 @@ left join guildconfigs on reactionrolemessage.guildconfigid = guildconfigs.id;")
|
|||||||
WHERE SendBoostMessage = TRUE;
|
WHERE SendBoostMessage = TRUE;
|
||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void AddGuildIdsToWarningPunishment(MigrationBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Sql("""
|
||||||
|
DELETE FROM WarningPunishment WHERE GuildConfigId IS NULL OR GuildConfigId NOT IN (SELECT Id FROM GuildConfigs);
|
||||||
|
UPDATE WarningPunishment
|
||||||
|
SET GuildId = (SELECT GuildId FROM GuildConfigs WHERE Id = GuildConfigId);
|
||||||
|
|
||||||
|
DELETE FROM WarningPunishment as wp
|
||||||
|
WHERE (wp.Count, wp.GuildConfigId) in (
|
||||||
|
SELECT wp2.Count, wp2.GuildConfigId FROM WarningPunishment as wp2
|
||||||
|
GROUP BY wp2.Count, wp2.GuildConfigId
|
||||||
|
HAVING COUNT(id) > 1
|
||||||
|
);
|
||||||
|
""");
|
||||||
|
}
|
||||||
}
|
}
|
3778
src/NadekoBot/Migrations/PostgreSql/20241018004623_warn-split.Designer.cs
generated
Normal file
3778
src/NadekoBot/Migrations/PostgreSql/20241018004623_warn-split.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations.PostgreSql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class warnsplit : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "guildid",
|
||||||
|
table: "warningpunishment",
|
||||||
|
type: "numeric(20,0)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0m);
|
||||||
|
|
||||||
|
MigrationQueries.AddGuildIdsToWarningPunishment(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "fk_warningpunishment_guildconfigs_guildconfigid",
|
||||||
|
table: "warningpunishment");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_warningpunishment_guildconfigid",
|
||||||
|
table: "warningpunishment");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "guildconfigid",
|
||||||
|
table: "warningpunishment");
|
||||||
|
|
||||||
|
migrationBuilder.AddUniqueConstraint(
|
||||||
|
name: "ak_warningpunishment_guildid_count",
|
||||||
|
table: "warningpunishment",
|
||||||
|
columns: new[] { "guildid", "count" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropUniqueConstraint(
|
||||||
|
name: "ak_warningpunishment_guildid_count",
|
||||||
|
table: "warningpunishment");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "guildid",
|
||||||
|
table: "warningpunishment");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "guildconfigid",
|
||||||
|
table: "warningpunishment",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_warningpunishment_guildconfigid",
|
||||||
|
table: "warningpunishment",
|
||||||
|
column: "guildconfigid");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "fk_warningpunishment_guildconfigs_guildconfigid",
|
||||||
|
table: "warningpunishment",
|
||||||
|
column: "guildconfigid",
|
||||||
|
principalTable: "guildconfigs",
|
||||||
|
principalColumn: "id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3824
src/NadekoBot/Migrations/PostgreSql/20241028033704_ncanvas.Designer.cs
generated
Normal file
3824
src/NadekoBot/Migrations/PostgreSql/20241028033704_ncanvas.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations.PostgreSql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ncanvas : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ncpixel",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
position = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
price = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
ownerid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
color = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
text = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_ncpixel", x => x.id);
|
||||||
|
table.UniqueConstraint("ak_ncpixel_position", x => x.position);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_discorduser_username",
|
||||||
|
table: "discorduser",
|
||||||
|
column: "username");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_ncpixel_ownerid",
|
||||||
|
table: "ncpixel",
|
||||||
|
column: "ownerid");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ncpixel");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "ix_discorduser_username",
|
||||||
|
table: "discorduser");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -799,6 +799,9 @@ namespace NadekoBot.Migrations.PostgreSql
|
|||||||
b.HasIndex("UserId")
|
b.HasIndex("UserId")
|
||||||
.HasDatabaseName("ix_discorduser_userid");
|
.HasDatabaseName("ix_discorduser_userid");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.HasDatabaseName("ix_discorduser_username");
|
||||||
|
|
||||||
b.ToTable("discorduser", (string)null);
|
b.ToTable("discorduser", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1627,6 +1630,49 @@ namespace NadekoBot.Migrations.PostgreSql
|
|||||||
b.ToTable("muteduserid", (string)null);
|
b.ToTable("muteduserid", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NadekoBot.Db.Models.NCPixel", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<long>("Color")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("color");
|
||||||
|
|
||||||
|
b.Property<decimal>("OwnerId")
|
||||||
|
.HasColumnType("numeric(20,0)")
|
||||||
|
.HasColumnName("ownerid");
|
||||||
|
|
||||||
|
b.Property<int>("Position")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("position");
|
||||||
|
|
||||||
|
b.Property<long>("Price")
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("price");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)")
|
||||||
|
.HasColumnName("text");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk_ncpixel");
|
||||||
|
|
||||||
|
b.HasAlternateKey("Position")
|
||||||
|
.HasName("ak_ncpixel_position");
|
||||||
|
|
||||||
|
b.HasIndex("OwnerId")
|
||||||
|
.HasDatabaseName("ix_ncpixel_ownerid");
|
||||||
|
|
||||||
|
b.ToTable("ncpixel", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.NadekoExpression", b =>
|
modelBuilder.Entity("NadekoBot.Db.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -2938,9 +2984,9 @@ namespace NadekoBot.Migrations.PostgreSql
|
|||||||
.HasColumnType("timestamp without time zone")
|
.HasColumnType("timestamp without time zone")
|
||||||
.HasColumnName("dateadded");
|
.HasColumnName("dateadded");
|
||||||
|
|
||||||
b.Property<int?>("GuildConfigId")
|
b.Property<decimal>("GuildId")
|
||||||
.HasColumnType("integer")
|
.HasColumnType("numeric(20,0)")
|
||||||
.HasColumnName("guildconfigid");
|
.HasColumnName("guildid");
|
||||||
|
|
||||||
b.Property<int>("Punishment")
|
b.Property<int>("Punishment")
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
@@ -2957,8 +3003,8 @@ namespace NadekoBot.Migrations.PostgreSql
|
|||||||
b.HasKey("Id")
|
b.HasKey("Id")
|
||||||
.HasName("pk_warningpunishment");
|
.HasName("pk_warningpunishment");
|
||||||
|
|
||||||
b.HasIndex("GuildConfigId")
|
b.HasAlternateKey("GuildId", "Count")
|
||||||
.HasDatabaseName("ix_warningpunishment_guildconfigid");
|
.HasName("ak_warningpunishment_guildid_count");
|
||||||
|
|
||||||
b.ToTable("warningpunishment", (string)null);
|
b.ToTable("warningpunishment", (string)null);
|
||||||
});
|
});
|
||||||
@@ -3616,15 +3662,6 @@ namespace NadekoBot.Migrations.PostgreSql
|
|||||||
b.Navigation("User");
|
b.Navigation("User");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.WarningPunishment", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("NadekoBot.Db.Models.GuildConfig", null)
|
|
||||||
.WithMany("WarnPunishments")
|
|
||||||
.HasForeignKey("GuildConfigId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.HasConstraintName("fk_warningpunishment_guildconfigs_guildconfigid");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.XpCurrencyReward", b =>
|
modelBuilder.Entity("NadekoBot.Db.Models.XpCurrencyReward", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("NadekoBot.Db.Models.XpSettings", "XpSettings")
|
b.HasOne("NadekoBot.Db.Models.XpSettings", "XpSettings")
|
||||||
@@ -3740,8 +3777,6 @@ namespace NadekoBot.Migrations.PostgreSql
|
|||||||
|
|
||||||
b.Navigation("VcRoleInfos");
|
b.Navigation("VcRoleInfos");
|
||||||
|
|
||||||
b.Navigation("WarnPunishments");
|
|
||||||
|
|
||||||
b.Navigation("XpSettings");
|
b.Navigation("XpSettings");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
2919
src/NadekoBot/Migrations/Sqlite/20241018004612_warn-split.Designer.cs
generated
Normal file
2919
src/NadekoBot/Migrations/Sqlite/20241018004612_warn-split.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
72
src/NadekoBot/Migrations/Sqlite/20241018004612_warn-split.cs
Normal file
72
src/NadekoBot/Migrations/Sqlite/20241018004612_warn-split.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class warnsplit : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<ulong>(
|
||||||
|
name: "GuildId",
|
||||||
|
table: "WarningPunishment",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0ul);
|
||||||
|
|
||||||
|
MigrationQueries.AddGuildIdsToWarningPunishment(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_WarningPunishment_GuildConfigs_GuildConfigId",
|
||||||
|
table: "WarningPunishment");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_WarningPunishment_GuildConfigId",
|
||||||
|
table: "WarningPunishment");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GuildConfigId",
|
||||||
|
table: "WarningPunishment");
|
||||||
|
|
||||||
|
migrationBuilder.AddUniqueConstraint(
|
||||||
|
name: "AK_WarningPunishment_GuildId_Count",
|
||||||
|
table: "WarningPunishment",
|
||||||
|
columns: new[] { "GuildId", "Count" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropUniqueConstraint(
|
||||||
|
name: "AK_WarningPunishment_GuildId_Count",
|
||||||
|
table: "WarningPunishment");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GuildId",
|
||||||
|
table: "WarningPunishment");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "GuildConfigId",
|
||||||
|
table: "WarningPunishment",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_WarningPunishment_GuildConfigId",
|
||||||
|
table: "WarningPunishment",
|
||||||
|
column: "GuildConfigId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_WarningPunishment_GuildConfigs_GuildConfigId",
|
||||||
|
table: "WarningPunishment",
|
||||||
|
column: "GuildConfigId",
|
||||||
|
principalTable: "GuildConfigs",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2953
src/NadekoBot/Migrations/Sqlite/20241028033656_ncanvas.Designer.cs
generated
Normal file
2953
src/NadekoBot/Migrations/Sqlite/20241028033656_ncanvas.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
src/NadekoBot/Migrations/Sqlite/20241028033656_ncanvas.cs
Normal file
53
src/NadekoBot/Migrations/Sqlite/20241028033656_ncanvas.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ncanvas : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "NCPixel",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
Position = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Price = table.Column<long>(type: "INTEGER", nullable: false),
|
||||||
|
OwnerId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
|
Color = table.Column<uint>(type: "INTEGER", nullable: false),
|
||||||
|
Text = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_NCPixel", x => x.Id);
|
||||||
|
table.UniqueConstraint("AK_NCPixel_Position", x => x.Position);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DiscordUser_Username",
|
||||||
|
table: "DiscordUser",
|
||||||
|
column: "Username");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_NCPixel_OwnerId",
|
||||||
|
table: "NCPixel",
|
||||||
|
column: "OwnerId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "NCPixel");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_DiscordUser_Username",
|
||||||
|
table: "DiscordUser");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -596,6 +596,8 @@ namespace NadekoBot.Migrations
|
|||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("Username");
|
||||||
|
|
||||||
b.ToTable("DiscordUser");
|
b.ToTable("DiscordUser");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1213,6 +1215,38 @@ namespace NadekoBot.Migrations
|
|||||||
b.ToTable("MutedUserId");
|
b.ToTable("MutedUserId");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NadekoBot.Db.Models.NCPixel", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<uint>("Color")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<ulong>("OwnerId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<int>("Position")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<long>("Price")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Text")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasAlternateKey("Position");
|
||||||
|
|
||||||
|
b.HasIndex("OwnerId");
|
||||||
|
|
||||||
|
b.ToTable("NCPixel");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.NadekoExpression", b =>
|
modelBuilder.Entity("NadekoBot.Db.Models.NadekoExpression", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -2183,7 +2217,7 @@ namespace NadekoBot.Migrations
|
|||||||
b.Property<DateTime?>("DateAdded")
|
b.Property<DateTime?>("DateAdded")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int?>("GuildConfigId")
|
b.Property<ulong>("GuildId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("Punishment")
|
b.Property<int>("Punishment")
|
||||||
@@ -2197,7 +2231,7 @@ namespace NadekoBot.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("GuildConfigId");
|
b.HasAlternateKey("GuildId", "Count");
|
||||||
|
|
||||||
b.ToTable("WarningPunishment");
|
b.ToTable("WarningPunishment");
|
||||||
});
|
});
|
||||||
@@ -2760,14 +2794,6 @@ namespace NadekoBot.Migrations
|
|||||||
b.Navigation("User");
|
b.Navigation("User");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.WarningPunishment", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("NadekoBot.Db.Models.GuildConfig", null)
|
|
||||||
.WithMany("WarnPunishments")
|
|
||||||
.HasForeignKey("GuildConfigId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.XpCurrencyReward", b =>
|
modelBuilder.Entity("NadekoBot.Db.Models.XpCurrencyReward", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("NadekoBot.Db.Models.XpSettings", "XpSettings")
|
b.HasOne("NadekoBot.Db.Models.XpSettings", "XpSettings")
|
||||||
@@ -2880,8 +2906,6 @@ namespace NadekoBot.Migrations
|
|||||||
|
|
||||||
b.Navigation("VcRoleInfos");
|
b.Navigation("VcRoleInfos");
|
||||||
|
|
||||||
b.Navigation("WarnPunishments");
|
|
||||||
|
|
||||||
b.Navigation("XpSettings");
|
b.Navigation("XpSettings");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -24,6 +24,13 @@ public partial class Administration
|
|||||||
await Response().Error(strs.hierarchy).SendAsync();
|
await Response().Error(strs.hierarchy).SendAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the user can't aar the role which is greater or equal to the bot's highest role
|
||||||
|
if (role.Position >= ((SocketGuild)ctx.Guild).CurrentUser.GetRoles().Max(x => x.Position))
|
||||||
|
{
|
||||||
|
await Response().Error(strs.hierarchy).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var roles = await _service.ToggleAarAsync(ctx.Guild.Id, role.Id);
|
var roles = await _service.ToggleAarAsync(ctx.Guild.Id, role.Id);
|
||||||
if (roles.Count == 0)
|
if (roles.Count == 0)
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.EntityFrameworkCore;
|
using LinqToDB.EntityFrameworkCore;
|
||||||
using NadekoBot.Common.ModuleBehaviors;
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
@@ -11,53 +10,63 @@ public class AutoPublishService : IExecNoCommand, IReadyExecutor, INService
|
|||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly IBotCredsProvider _creds;
|
private readonly IBotCredsProvider _creds;
|
||||||
private ConcurrentDictionary<ulong, ulong> _enabled;
|
private ConcurrentDictionary<ulong, ulong> _enabled = new();
|
||||||
|
|
||||||
public AutoPublishService(DbService db, DiscordSocketClient client, IBotCredsProvider creds)
|
public AutoPublishService(DbService db, DiscordSocketClient client, IBotCredsProvider creds)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_client = client;
|
_client = client;
|
||||||
_creds = creds;
|
_creds = creds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
|
public async Task ExecOnNoCommandAsync(IGuild? guild, IUserMessage msg)
|
||||||
{
|
{
|
||||||
if (guild is null)
|
if (guild is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (msg.Channel.GetChannelType() != ChannelType.News)
|
if (msg.Channel.GetChannelType() != ChannelType.News)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!_enabled.TryGetValue(guild.Id, out var cid) || cid != msg.Channel.Id)
|
if (!_enabled.TryGetValue(guild.Id, out var cid) || cid != msg.Channel.Id)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await msg.CrosspostAsync(new RequestOptions()
|
await msg.CrosspostAsync(new RequestOptions()
|
||||||
{
|
{
|
||||||
RetryMode = RetryMode.AlwaysFail
|
RetryMode = RetryMode.AlwaysFail
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo GUILDS
|
|
||||||
|
|
||||||
public async Task OnReadyAsync()
|
public async Task OnReadyAsync()
|
||||||
{
|
{
|
||||||
var creds = _creds.GetCreds();
|
var creds = _creds.GetCreds();
|
||||||
|
|
||||||
await using var ctx = _db.GetDbContext();
|
await using var ctx = _db.GetDbContext();
|
||||||
var items = await ctx.GetTable<AutoPublishChannel>()
|
var items = await ctx.GetTable<AutoPublishChannel>()
|
||||||
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId, creds.TotalShards, _client.ShardId))
|
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId, creds.TotalShards, _client.ShardId))
|
||||||
.ToListAsyncLinqToDB();
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
_enabled = items
|
_enabled = items
|
||||||
.ToDictionary(x => x.GuildId, x => x.ChannelId)
|
.ToDictionary(x => x.GuildId, x => x.ChannelId)
|
||||||
.ToConcurrent();
|
.ToConcurrent();
|
||||||
|
|
||||||
|
_client.LeftGuild += ClientOnLeftGuild;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ClientOnLeftGuild(SocketGuild guild)
|
||||||
|
{
|
||||||
|
await using var ctx = _db.GetDbContext();
|
||||||
|
_enabled.TryRemove(guild.Id, out _);
|
||||||
|
|
||||||
|
await ctx.GetTable<AutoPublishChannel>()
|
||||||
|
.Where(x => x.GuildId == guild.Id)
|
||||||
|
.DeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> ToggleAutoPublish(ulong guildId, ulong channelId)
|
public async Task<bool> ToggleAutoPublish(ulong guildId, ulong channelId)
|
||||||
{
|
{
|
||||||
await using var ctx = _db.GetDbContext();
|
await using var ctx = _db.GetDbContext();
|
||||||
var deleted = await ctx.GetTable<AutoPublishChannel>()
|
var deleted = await ctx.GetTable<AutoPublishChannel>()
|
||||||
.DeleteAsync(x => x.GuildId == guildId && x.ChannelId == channelId);
|
.DeleteAsync(x => x.GuildId == guildId && x.ChannelId == channelId);
|
||||||
|
|
||||||
if (deleted != 0)
|
if (deleted != 0)
|
||||||
{
|
{
|
||||||
@@ -66,22 +75,22 @@ public class AutoPublishService : IExecNoCommand, IReadyExecutor, INService
|
|||||||
}
|
}
|
||||||
|
|
||||||
await ctx.GetTable<AutoPublishChannel>()
|
await ctx.GetTable<AutoPublishChannel>()
|
||||||
.InsertOrUpdateAsync(() => new()
|
.InsertOrUpdateAsync(() => new()
|
||||||
{
|
{
|
||||||
GuildId = guildId,
|
GuildId = guildId,
|
||||||
ChannelId = channelId,
|
ChannelId = channelId,
|
||||||
DateAdded = DateTime.UtcNow,
|
DateAdded = DateTime.UtcNow,
|
||||||
},
|
},
|
||||||
old => new()
|
old => new()
|
||||||
{
|
{
|
||||||
ChannelId = channelId,
|
ChannelId = channelId,
|
||||||
DateAdded = DateTime.UtcNow,
|
DateAdded = DateTime.UtcNow,
|
||||||
},
|
},
|
||||||
() => new()
|
() => new()
|
||||||
{
|
{
|
||||||
GuildId = guildId
|
GuildId = guildId
|
||||||
});
|
});
|
||||||
|
|
||||||
_enabled[guildId] = channelId;
|
_enabled[guildId] = channelId;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@@ -206,6 +206,18 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, INService
|
|||||||
.Where(x => !tempTable.Select(x => x.GuildId)
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
.Contains(x.GuildId))
|
.Contains(x.GuildId))
|
||||||
.DeleteAsync();
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete autopublish channels
|
||||||
|
await ctx.GetTable<AutoPublishChannel>()
|
||||||
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete greet settings
|
||||||
|
await ctx.GetTable<GreetSettings>()
|
||||||
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
|
@@ -138,8 +138,7 @@ public partial class Administration
|
|||||||
[OwnerOnly]
|
[OwnerOnly]
|
||||||
public Task DeleteXp()
|
public Task DeleteXp()
|
||||||
=> ConfirmActionInternalAsync("Delete Xp", () => _xcs.DeleteXp());
|
=> ConfirmActionInternalAsync("Delete Xp", () => _xcs.DeleteXp());
|
||||||
|
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[OwnerOnly]
|
[OwnerOnly]
|
||||||
public Task DeleteWaifus()
|
public Task DeleteWaifus()
|
||||||
|
@@ -200,9 +200,7 @@ public partial class Administration
|
|||||||
|
|
||||||
if (!isEnabled)
|
if (!isEnabled)
|
||||||
{
|
{
|
||||||
var cmdName = GetCmdName(type);
|
await SendGreetEnableHint(type);
|
||||||
|
|
||||||
await Response().Pending(strs.boostmsg_enable($"`{prefix}{cmdName}`")).SendAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,19 +224,24 @@ public partial class Administration
|
|||||||
await _service.Test(ctx.Guild.Id, type, (ITextChannel)ctx.Channel, user);
|
await _service.Test(ctx.Guild.Id, type, (ITextChannel)ctx.Channel, user);
|
||||||
var conf = await _service.GetGreetSettingsAsync(ctx.Guild.Id, type);
|
var conf = await _service.GetGreetSettingsAsync(ctx.Guild.Id, type);
|
||||||
|
|
||||||
|
if (conf?.IsEnabled is not true)
|
||||||
|
await SendGreetEnableHint(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendGreetEnableHint(GreetType type)
|
||||||
|
{
|
||||||
var cmd = $"`{prefix}{GetCmdName(type)}`";
|
var cmd = $"`{prefix}{GetCmdName(type)}`";
|
||||||
|
|
||||||
var str = type switch
|
var str = type switch
|
||||||
{
|
{
|
||||||
GreetType.Greet => strs.boostmsg_enable(cmd),
|
GreetType.Greet => strs.greetmsg_enable(cmd),
|
||||||
GreetType.Bye => strs.greetmsg_enable(cmd),
|
GreetType.Bye => strs.byemsg_enable(cmd),
|
||||||
GreetType.Boost => strs.byemsg_enable(cmd),
|
GreetType.Boost => strs.boostmsg_enable(cmd),
|
||||||
GreetType.GreetDm => strs.greetdmmsg_enable(cmd),
|
GreetType.GreetDm => strs.greetdmmsg_enable(cmd),
|
||||||
_ => strs.error
|
_ => strs.error
|
||||||
};
|
};
|
||||||
|
|
||||||
if (conf?.IsEnabled is not true)
|
await Response().Pending(str).SendAsync();
|
||||||
await Response().Pending(str).SendAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -75,16 +75,27 @@ public class GreetService : INService, IReadyExecutor
|
|||||||
|
|
||||||
_client.GuildMemberUpdated += ClientOnGuildMemberUpdated;
|
_client.GuildMemberUpdated += ClientOnGuildMemberUpdated;
|
||||||
|
|
||||||
var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));
|
while (true)
|
||||||
while (await timer.WaitForNextTickAsync())
|
|
||||||
{
|
{
|
||||||
var (conf, user, ch) = await _greetQueue.Reader.ReadAsync();
|
try
|
||||||
await GreetUsers(conf, ch, user);
|
{
|
||||||
|
var (conf, user, ch) = await _greetQueue.Reader.ReadAsync();
|
||||||
|
await GreetUsers(conf, ch, user);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Greet Loop almost crashed. Please report this!");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(2016);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task ClientOnGuildMemberUpdated(Cacheable<SocketGuildUser, ulong> optOldUser, SocketGuildUser newUser)
|
private Task ClientOnGuildMemberUpdated(Cacheable<SocketGuildUser, ulong> optOldUser, SocketGuildUser newUser)
|
||||||
{
|
{
|
||||||
|
if (!_enabled[GreetType.Boost].Contains(newUser.Guild.Id))
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
// if user is a new booster
|
// if user is a new booster
|
||||||
// or boosted again the same server
|
// or boosted again the same server
|
||||||
if ((optOldUser.Value is { PremiumSince: null } && newUser is { PremiumSince: not null })
|
if ((optOldUser.Value is { PremiumSince: null } && newUser is { PremiumSince: not null })
|
||||||
@@ -126,21 +137,63 @@ public class GreetService : INService, IReadyExecutor
|
|||||||
.DeleteAsync();
|
.DeleteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task OnUserLeft(SocketGuild guild, SocketUser user)
|
private Task OnUserJoined(IGuildUser user)
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_enabled[GreetType.Greet].Contains(user.GuildId))
|
||||||
|
{
|
||||||
|
var conf = await GetGreetSettingsAsync(user.GuildId, GreetType.Greet);
|
||||||
|
if (conf?.ChannelId is ulong cid)
|
||||||
|
{
|
||||||
|
var channel = await user.Guild.GetTextChannelAsync(cid);
|
||||||
|
if (channel is not null)
|
||||||
|
{
|
||||||
|
await _greetQueue.Writer.WriteAsync((conf, user, channel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (_enabled[GreetType.GreetDm].Contains(user.GuildId))
|
||||||
|
{
|
||||||
|
var confDm = await GetGreetSettingsAsync(user.GuildId, GreetType.GreetDm);
|
||||||
|
if (confDm is not null)
|
||||||
|
{
|
||||||
|
await _greetQueue.Writer.WriteAsync((confDm, user, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Error in GreetService.OnUserJoined. This should not happen. Please report it");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task OnUserLeft(SocketGuild guild, SocketUser user)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
if (!_enabled[GreetType.Bye].Contains(guild.Id))
|
||||||
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var conf = await GetGreetSettingsAsync(guild.Id, GreetType.Bye);
|
var conf = await GetGreetSettingsAsync(guild.Id, GreetType.Bye);
|
||||||
|
|
||||||
if (conf is null)
|
if (conf?.ChannelId is not { } cid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var channel = guild.TextChannels.FirstOrDefault(c => c.Id == conf.ChannelId);
|
var channel = guild.GetChannel(cid) as ITextChannel;
|
||||||
|
|
||||||
if (channel is null) //maybe warn the server owner that the channel is missing
|
if (channel is null) //maybe warn the server owner that the channel is missing
|
||||||
{
|
{
|
||||||
|
Log.Warning("Channel {ChannelId} in {GuildId} was not found. Bye message will be disabled",
|
||||||
|
conf.ChannelId,
|
||||||
|
conf.GuildId);
|
||||||
await SetGreet(guild.Id, null, GreetType.Bye, false);
|
await SetGreet(guild.Id, null, GreetType.Bye, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -155,10 +208,11 @@ public class GreetService : INService, IReadyExecutor
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly TypedKey<GreetSettings?> _greetSettingsKey = new("greet_settings");
|
private TypedKey<GreetSettings?> GreetSettingsKey(ulong gid, GreetType type)
|
||||||
|
=> new($"greet_settings:{gid}:{type}");
|
||||||
|
|
||||||
public async Task<GreetSettings?> GetGreetSettingsAsync(ulong gid, GreetType type)
|
public async Task<GreetSettings?> GetGreetSettingsAsync(ulong gid, GreetType type)
|
||||||
=> await _cache.GetOrAddAsync<GreetSettings?>(_greetSettingsKey,
|
=> await _cache.GetOrAddAsync<GreetSettings?>(GreetSettingsKey(gid, type),
|
||||||
() => InternalGetGreetSettingsAsync(gid, type),
|
() => InternalGetGreetSettingsAsync(gid, type),
|
||||||
TimeSpan.FromSeconds(3));
|
TimeSpan.FromSeconds(3));
|
||||||
|
|
||||||
@@ -207,9 +261,10 @@ public class GreetService : INService, IReadyExecutor
|
|||||||
or DiscordErrorCode.UnknownChannel)
|
or DiscordErrorCode.UnknownChannel)
|
||||||
{
|
{
|
||||||
Log.Warning(ex,
|
Log.Warning(ex,
|
||||||
"Missing permissions to send a bye message, the greet message will be disabled on server: {GuildId}",
|
"Missing permissions to send a {GreetType} message, it will be disabled on server: {GuildId}",
|
||||||
|
conf.GreetType,
|
||||||
channel.GuildId);
|
channel.GuildId);
|
||||||
await SetGreet(channel.GuildId, channel.Id, GreetType.Greet, false);
|
await SetGreet(channel.GuildId, channel.Id, conf.GreetType, false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -217,14 +272,6 @@ public class GreetService : INService, IReadyExecutor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<bool> GreetDmUser(GreetSettings conf, IGuildUser user)
|
|
||||||
{
|
|
||||||
var completionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
||||||
await _greetQueue.Writer.WriteAsync((conf, user, null));
|
|
||||||
return await completionSource.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> GreetDmUserInternal(GreetSettings conf, IGuildUser user)
|
private async Task<bool> GreetDmUserInternal(GreetSettings conf, IGuildUser user)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -290,8 +337,9 @@ public class GreetService : INService, IReadyExecutor
|
|||||||
|
|
||||||
await _sender.Response(user).Text(smartText).Sanitize(false).SendAsync();
|
await _sender.Response(user).Text(smartText).Sanitize(false).SendAsync();
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
Log.Warning(ex, "Unable to send Greet DM. Probably the user has closed DMs");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,36 +353,6 @@ public class GreetService : INService, IReadyExecutor
|
|||||||
IconUrl = user.Guild.IconUrl
|
IconUrl = user.Guild.IconUrl
|
||||||
};
|
};
|
||||||
|
|
||||||
private Task OnUserJoined(IGuildUser user)
|
|
||||||
{
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var conf = await GetGreetSettingsAsync(user.GuildId, GreetType.Greet);
|
|
||||||
|
|
||||||
if (conf is not null && conf.IsEnabled && conf.ChannelId is { } channelId)
|
|
||||||
{
|
|
||||||
var channel = await user.Guild.GetTextChannelAsync(channelId);
|
|
||||||
if (channel is not null)
|
|
||||||
{
|
|
||||||
await _greetQueue.Writer.WriteAsync((conf, user, channel));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var confDm = await GetGreetSettingsAsync(user.GuildId, GreetType.GreetDm);
|
|
||||||
|
|
||||||
if (confDm?.IsEnabled ?? false)
|
|
||||||
await GreetDmUser(confDm, user);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static string GetDefaultGreet(GreetType greetType)
|
public static string GetDefaultGreet(GreetType greetType)
|
||||||
=> greetType switch
|
=> greetType switch
|
||||||
@@ -354,8 +372,8 @@ public class GreetService : INService, IReadyExecutor
|
|||||||
{
|
{
|
||||||
await using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
var q = uow.GetTable<GreetSettings>();
|
var q = uow.GetTable<GreetSettings>();
|
||||||
|
|
||||||
if(value is null)
|
if (value is null)
|
||||||
value = !_enabled[greetType].Contains(guildId);
|
value = !_enabled[greetType].Contains(guildId);
|
||||||
|
|
||||||
if (value is { } v)
|
if (value is { } v)
|
||||||
@@ -477,7 +495,8 @@ public class GreetService : INService, IReadyExecutor
|
|||||||
{
|
{
|
||||||
if (conf.GreetType == GreetType.GreetDm)
|
if (conf.GreetType == GreetType.GreetDm)
|
||||||
{
|
{
|
||||||
return await GreetDmUser(conf, user);
|
await _greetQueue.Writer.WriteAsync((conf, user, null));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channel is not ITextChannel ch)
|
if (channel is not ITextChannel ch)
|
||||||
|
@@ -13,7 +13,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
|
|||||||
{
|
{
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
|
|
||||||
private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
|
private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
|
||||||
private readonly object _cacheLock = new();
|
private readonly object _cacheLock = new();
|
||||||
@@ -24,7 +24,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
|
|||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
IPatronageService ps,
|
IPatronageService ps,
|
||||||
DbService db,
|
DbService db,
|
||||||
IBotCredentials creds)
|
IBotCreds creds)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_client = client;
|
_client = client;
|
||||||
|
@@ -9,13 +9,13 @@ namespace NadekoBot.Modules.Administration;
|
|||||||
public sealed class StickyRolesService : INService, IReadyExecutor
|
public sealed class StickyRolesService : INService, IReadyExecutor
|
||||||
{
|
{
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
private HashSet<ulong> _stickyRoles = new();
|
private HashSet<ulong> _stickyRoles = new();
|
||||||
|
|
||||||
public StickyRolesService(
|
public StickyRolesService(
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
IBotCredentials creds,
|
IBotCreds creds,
|
||||||
DbService db)
|
DbService db)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
|
@@ -15,7 +15,7 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
|
|||||||
private readonly IBotStrings _strings;
|
private readonly IBotStrings _strings;
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
|
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
|
|
||||||
private ImmutableDictionary<ulong, IDMChannel> ownerChannels =
|
private ImmutableDictionary<ulong, IDMChannel> ownerChannels =
|
||||||
new Dictionary<ulong, IDMChannel>().ToImmutableDictionary();
|
new Dictionary<ulong, IDMChannel>().ToImmutableDictionary();
|
||||||
@@ -36,7 +36,7 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
|
|||||||
CommandHandler cmdHandler,
|
CommandHandler cmdHandler,
|
||||||
DbService db,
|
DbService db,
|
||||||
IBotStrings strings,
|
IBotStrings strings,
|
||||||
IBotCredentials creds,
|
IBotCreds creds,
|
||||||
IHttpClientFactory factory,
|
IHttpClientFactory factory,
|
||||||
BotConfigService bss,
|
BotConfigService bss,
|
||||||
IPubSub pubSub,
|
IPubSub pubSub,
|
||||||
|
@@ -66,10 +66,10 @@ public partial class Administration
|
|||||||
{
|
{
|
||||||
await _sender.Response(user)
|
await _sender.Response(user)
|
||||||
.Embed(_sender.CreateEmbed()
|
.Embed(_sender.CreateEmbed()
|
||||||
.WithErrorColor()
|
.WithErrorColor()
|
||||||
.WithDescription(GetText(strs.warned_on(ctx.Guild.ToString())))
|
.WithDescription(GetText(strs.warned_on(ctx.Guild.ToString())))
|
||||||
.AddField(GetText(strs.moderator), ctx.User.ToString())
|
.AddField(GetText(strs.moderator), ctx.User.ToString())
|
||||||
.AddField(GetText(strs.reason), reason ?? "-"))
|
.AddField(GetText(strs.reason), reason ?? "-"))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -85,8 +85,9 @@ public partial class Administration
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Warning(ex, "Exception occured while warning a user");
|
Log.Warning(ex, "Exception occured while warning a user");
|
||||||
var errorEmbed = _sender.CreateEmbed().WithErrorColor()
|
var errorEmbed = _sender.CreateEmbed()
|
||||||
.WithDescription(GetText(strs.cant_apply_punishment));
|
.WithErrorColor()
|
||||||
|
.WithDescription(GetText(strs.cant_apply_punishment));
|
||||||
|
|
||||||
if (dmFailed)
|
if (dmFailed)
|
||||||
errorEmbed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
errorEmbed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
||||||
@@ -117,7 +118,7 @@ public partial class Administration
|
|||||||
[Priority(1)]
|
[Priority(1)]
|
||||||
public async Task WarnExpire()
|
public async Task WarnExpire()
|
||||||
{
|
{
|
||||||
var expireDays = await _service.GetWarnExpire(ctx.Guild.Id);
|
var (expireDays, _) = await _service.GetWarnExpire(ctx.Guild.Id);
|
||||||
|
|
||||||
if (expireDays == 0)
|
if (expireDays == 0)
|
||||||
await Response().Confirm(strs.warns_dont_expire).SendAsync();
|
await Response().Confirm(strs.warns_dont_expire).SendAsync();
|
||||||
@@ -266,9 +267,9 @@ public partial class Administration
|
|||||||
});
|
});
|
||||||
|
|
||||||
return _sender.CreateEmbed()
|
return _sender.CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithTitle(GetText(strs.warnings_list))
|
.WithTitle(GetText(strs.warnings_list))
|
||||||
.WithDescription(string.Join("\n", ws));
|
.WithDescription(string.Join("\n", ws));
|
||||||
})
|
})
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
@@ -278,7 +279,7 @@ public partial class Administration
|
|||||||
[UserPerm(GuildPerm.Administrator)]
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
public Task WarnDelete(IGuildUser user, int index)
|
public Task WarnDelete(IGuildUser user, int index)
|
||||||
=> WarnDelete(user.Id, index);
|
=> WarnDelete(user.Id, index);
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[UserPerm(GuildPerm.Administrator)]
|
[UserPerm(GuildPerm.Administrator)]
|
||||||
@@ -286,15 +287,15 @@ public partial class Administration
|
|||||||
{
|
{
|
||||||
if (--index < 0)
|
if (--index < 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var warn = await _service.WarnDelete(userId, index);
|
var warn = await _service.WarnDelete(ctx.Guild.Id, userId, index);
|
||||||
|
|
||||||
if (warn is null)
|
if (warn is null)
|
||||||
{
|
{
|
||||||
await Response().Error(strs.warning_not_found).SendAsync();
|
await Response().Error(strs.warning_not_found).SendAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Response().Confirm(strs.warning_deleted(Format.Bold(index.ToString()))).SendAsync();
|
await Response().Confirm(strs.warning_deleted(Format.Bold(index.ToString()))).SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +312,7 @@ public partial class Administration
|
|||||||
{
|
{
|
||||||
if (index < 0)
|
if (index < 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var success = await _service.WarnClearAsync(ctx.Guild.Id, userId, index, ctx.User.ToString());
|
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());
|
var userStr = Format.Bold((ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString());
|
||||||
if (index == 0)
|
if (index == 0)
|
||||||
@@ -344,7 +345,7 @@ public partial class Administration
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time, role);
|
var success = await _service.WarnPunish(ctx.Guild.Id, number, punish, time, role);
|
||||||
|
|
||||||
if (!success)
|
if (!success)
|
||||||
return;
|
return;
|
||||||
@@ -380,7 +381,7 @@ public partial class Administration
|
|||||||
if (punish is PunishmentAction.TimeOut && time is null)
|
if (punish is PunishmentAction.TimeOut && time is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time);
|
var success = await _service.WarnPunish(ctx.Guild.Id, number, punish, time);
|
||||||
|
|
||||||
if (!success)
|
if (!success)
|
||||||
return;
|
return;
|
||||||
@@ -407,7 +408,7 @@ public partial class Administration
|
|||||||
[UserPerm(GuildPerm.BanMembers)]
|
[UserPerm(GuildPerm.BanMembers)]
|
||||||
public async Task WarnPunish(int number)
|
public async Task WarnPunish(int number)
|
||||||
{
|
{
|
||||||
if (!_service.WarnPunishRemove(ctx.Guild.Id, number))
|
if (!await _service.WarnPunishRemove(ctx.Guild.Id, number))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await Response().Confirm(strs.warn_punish_rem(Format.Bold(number.ToString()))).SendAsync();
|
await Response().Confirm(strs.warn_punish_rem(Format.Bold(number.ToString()))).SendAsync();
|
||||||
@@ -417,7 +418,7 @@ public partial class Administration
|
|||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
public async Task WarnPunishList()
|
public async Task WarnPunishList()
|
||||||
{
|
{
|
||||||
var ps = _service.WarnPunishList(ctx.Guild.Id);
|
var ps = await _service.WarnPunishList(ctx.Guild.Id);
|
||||||
|
|
||||||
string list;
|
string list;
|
||||||
if (ps.Any())
|
if (ps.Any())
|
||||||
@@ -478,13 +479,13 @@ public partial class Administration
|
|||||||
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
|
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
|
||||||
await _mute.TimedBan(ctx.Guild, userId, time.Time, (ctx.User + " | " + msg).TrimTo(512), banPrune);
|
await _mute.TimedBan(ctx.Guild, userId, time.Time, (ctx.User + " | " + msg).TrimTo(512), banPrune);
|
||||||
var toSend = _sender.CreateEmbed()
|
var toSend = _sender.CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||||
.AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true)
|
.AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true)
|
||||||
.AddField("ID", userId.ToString(), true)
|
.AddField("ID", userId.ToString(), true)
|
||||||
.AddField(GetText(strs.duration),
|
.AddField(GetText(strs.duration),
|
||||||
time.Time.ToPrettyStringHm(),
|
time.Time.ToPrettyStringHm(),
|
||||||
true);
|
true);
|
||||||
|
|
||||||
if (dmFailed)
|
if (dmFailed)
|
||||||
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
||||||
@@ -505,11 +506,12 @@ public partial class Administration
|
|||||||
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
|
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
|
||||||
await ctx.Guild.AddBanAsync(userId, banPrune, (ctx.User + " | " + msg).TrimTo(512));
|
await ctx.Guild.AddBanAsync(userId, banPrune, (ctx.User + " | " + msg).TrimTo(512));
|
||||||
|
|
||||||
await Response().Embed(_sender.CreateEmbed()
|
await Response()
|
||||||
.WithOkColor()
|
.Embed(_sender.CreateEmbed()
|
||||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
.WithOkColor()
|
||||||
.AddField("ID", userId.ToString(), true))
|
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||||
.SendAsync();
|
.AddField("ID", userId.ToString(), true))
|
||||||
|
.SendAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
await Ban(user, msg);
|
await Ban(user, msg);
|
||||||
@@ -543,10 +545,10 @@ public partial class Administration
|
|||||||
await ctx.Guild.AddBanAsync(user, banPrune, (ctx.User + " | " + msg).TrimTo(512));
|
await ctx.Guild.AddBanAsync(user, banPrune, (ctx.User + " | " + msg).TrimTo(512));
|
||||||
|
|
||||||
var toSend = _sender.CreateEmbed()
|
var toSend = _sender.CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
.WithTitle("⛔️ " + GetText(strs.banned_user))
|
||||||
.AddField(GetText(strs.username), user.ToString(), true)
|
.AddField(GetText(strs.username), user.ToString(), true)
|
||||||
.AddField("ID", user.Id.ToString(), true);
|
.AddField("ID", user.Id.ToString(), true);
|
||||||
|
|
||||||
if (dmFailed)
|
if (dmFailed)
|
||||||
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
||||||
@@ -738,10 +740,10 @@ public partial class Administration
|
|||||||
catch { await ctx.Guild.RemoveBanAsync(user); }
|
catch { await ctx.Guild.RemoveBanAsync(user); }
|
||||||
|
|
||||||
var toSend = _sender.CreateEmbed()
|
var toSend = _sender.CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithTitle("☣ " + GetText(strs.sb_user))
|
.WithTitle("☣ " + GetText(strs.sb_user))
|
||||||
.AddField(GetText(strs.username), user.ToString(), true)
|
.AddField(GetText(strs.username), user.ToString(), true)
|
||||||
.AddField("ID", user.Id.ToString(), true);
|
.AddField("ID", user.Id.ToString(), true);
|
||||||
|
|
||||||
if (dmFailed)
|
if (dmFailed)
|
||||||
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
||||||
@@ -793,10 +795,10 @@ public partial class Administration
|
|||||||
await user.KickAsync((ctx.User + " | " + msg).TrimTo(512));
|
await user.KickAsync((ctx.User + " | " + msg).TrimTo(512));
|
||||||
|
|
||||||
var toSend = _sender.CreateEmbed()
|
var toSend = _sender.CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithTitle(GetText(strs.kicked_user))
|
.WithTitle(GetText(strs.kicked_user))
|
||||||
.AddField(GetText(strs.username), user.ToString(), true)
|
.AddField(GetText(strs.username), user.ToString(), true)
|
||||||
.AddField("ID", user.Id.ToString(), true);
|
.AddField("ID", user.Id.ToString(), true);
|
||||||
|
|
||||||
if (dmFailed)
|
if (dmFailed)
|
||||||
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
||||||
@@ -826,8 +828,8 @@ public partial class Administration
|
|||||||
var dmMessage = GetText(strs.timeoutdm(Format.Bold(ctx.Guild.Name), msg));
|
var dmMessage = GetText(strs.timeoutdm(Format.Bold(ctx.Guild.Name), msg));
|
||||||
await _sender.Response(user)
|
await _sender.Response(user)
|
||||||
.Embed(_sender.CreateEmbed()
|
.Embed(_sender.CreateEmbed()
|
||||||
.WithPendingColor()
|
.WithPendingColor()
|
||||||
.WithDescription(dmMessage))
|
.WithDescription(dmMessage))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -838,10 +840,10 @@ public partial class Administration
|
|||||||
await user.SetTimeOutAsync(time.Time);
|
await user.SetTimeOutAsync(time.Time);
|
||||||
|
|
||||||
var toSend = _sender.CreateEmbed()
|
var toSend = _sender.CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithTitle("⏳ " + GetText(strs.timedout_user))
|
.WithTitle("⏳ " + GetText(strs.timedout_user))
|
||||||
.AddField(GetText(strs.username), user.ToString(), true)
|
.AddField(GetText(strs.username), user.ToString(), true)
|
||||||
.AddField("ID", user.Id.ToString(), true);
|
.AddField("ID", user.Id.ToString(), true);
|
||||||
|
|
||||||
if (dmFailed)
|
if (dmFailed)
|
||||||
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
|
||||||
@@ -899,9 +901,9 @@ public partial class Administration
|
|||||||
missStr = "-";
|
missStr = "-";
|
||||||
|
|
||||||
var toSend = _sender.CreateEmbed()
|
var toSend = _sender.CreateEmbed()
|
||||||
.WithDescription(GetText(strs.mass_ban_in_progress(banning.Count)))
|
.WithDescription(GetText(strs.mass_ban_in_progress(banning.Count)))
|
||||||
.AddField(GetText(strs.invalid(missing.Count)), missStr)
|
.AddField(GetText(strs.invalid(missing.Count)), missStr)
|
||||||
.WithPendingColor();
|
.WithPendingColor();
|
||||||
|
|
||||||
var banningMessage = await Response().Embed(toSend).SendAsync();
|
var banningMessage = await Response().Embed(toSend).SendAsync();
|
||||||
|
|
||||||
@@ -919,11 +921,13 @@ public partial class Administration
|
|||||||
}
|
}
|
||||||
|
|
||||||
await banningMessage.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
|
await banningMessage.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
|
||||||
.WithDescription(
|
.WithDescription(
|
||||||
GetText(strs.mass_ban_completed(banning.Count())))
|
GetText(strs.mass_ban_completed(
|
||||||
.AddField(GetText(strs.invalid(missing.Count)), missStr)
|
banning.Count())))
|
||||||
.WithOkColor()
|
.AddField(GetText(strs.invalid(missing.Count)),
|
||||||
.Build());
|
missStr)
|
||||||
|
.WithOkColor()
|
||||||
|
.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
@@ -945,10 +949,10 @@ public partial class Administration
|
|||||||
//send a message but don't wait for it
|
//send a message but don't wait for it
|
||||||
var banningMessageTask = Response()
|
var banningMessageTask = Response()
|
||||||
.Embed(_sender.CreateEmbed()
|
.Embed(_sender.CreateEmbed()
|
||||||
.WithDescription(
|
.WithDescription(
|
||||||
GetText(strs.mass_kill_in_progress(bans.Count())))
|
GetText(strs.mass_kill_in_progress(bans.Count())))
|
||||||
.AddField(GetText(strs.invalid(missing)), missStr)
|
.AddField(GetText(strs.invalid(missing)), missStr)
|
||||||
.WithPendingColor())
|
.WithPendingColor())
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
|
|
||||||
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
|
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
|
||||||
@@ -966,11 +970,11 @@ public partial class Administration
|
|||||||
var banningMessage = await banningMessageTask;
|
var banningMessage = await banningMessageTask;
|
||||||
|
|
||||||
await banningMessage.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
|
await banningMessage.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
|
||||||
.WithDescription(
|
.WithDescription(
|
||||||
GetText(strs.mass_kill_completed(bans.Count())))
|
GetText(strs.mass_kill_completed(bans.Count())))
|
||||||
.AddField(GetText(strs.invalid(missing)), missStr)
|
.AddField(GetText(strs.invalid(missing)), missStr)
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.Build());
|
.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WarnExpireOptions : INadekoCommandOptions
|
public class WarnExpireOptions : INadekoCommandOptions
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.EntityFrameworkCore;
|
using LinqToDB.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using NadekoBot.Common.ModuleBehaviors;
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
using NadekoBot.Common.TypeReaders.Models;
|
|
||||||
using NadekoBot.Modules.Permissions.Services;
|
using NadekoBot.Modules.Permissions.Services;
|
||||||
using NadekoBot.Db.Models;
|
using NadekoBot.Db.Models;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -83,17 +81,24 @@ public class UserPunishService : INService, IReadyExecutor
|
|||||||
};
|
};
|
||||||
|
|
||||||
long previousCount;
|
long previousCount;
|
||||||
List<WarningPunishment> ps;
|
var ps = await WarnPunishList(guildId);
|
||||||
await using (var uow = _db.GetDbContext())
|
await using (var uow = _db.GetDbContext())
|
||||||
{
|
{
|
||||||
ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
|
previousCount = uow.GetTable<Warning>()
|
||||||
|
.Where(w => w.GuildId == guildId && w.UserId == userId && !w.Forgiven)
|
||||||
previousCount = uow.Set<Warning>()
|
|
||||||
.ForId(guildId, userId)
|
|
||||||
.Where(w => !w.Forgiven && w.UserId == userId)
|
|
||||||
.Sum(x => x.Weight);
|
.Sum(x => x.Weight);
|
||||||
|
|
||||||
uow.Set<Warning>().Add(warn);
|
await uow.GetTable<Warning>()
|
||||||
|
.InsertAsync(() => new()
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
GuildId = guildId,
|
||||||
|
Forgiven = false,
|
||||||
|
Reason = reason,
|
||||||
|
Moderator = modName,
|
||||||
|
Weight = weight,
|
||||||
|
DateAdded = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
|
||||||
await uow.SaveChangesAsync();
|
await uow.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
@@ -260,12 +265,12 @@ public class UserPunishService : INService, IReadyExecutor
|
|||||||
.ToListAsyncLinqToDB();
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
var cleared = await uow.GetTable<Warning>()
|
var cleared = await uow.GetTable<Warning>()
|
||||||
.Where(x => toClear.Contains(x.Id))
|
.Where(x => toClear.Contains(x.Id))
|
||||||
.UpdateAsync(_ => new()
|
.UpdateAsync(_ => new()
|
||||||
{
|
{
|
||||||
Forgiven = true,
|
Forgiven = true,
|
||||||
ForgivenBy = "expiry"
|
ForgivenBy = "expiry"
|
||||||
});
|
});
|
||||||
|
|
||||||
var toDelete = await uow.GetTable<Warning>()
|
var toDelete = await uow.GetTable<Warning>()
|
||||||
.Where(x => uow.GetTable<GuildConfig>()
|
.Where(x => uow.GetTable<GuildConfig>()
|
||||||
@@ -282,8 +287,8 @@ public class UserPunishService : INService, IReadyExecutor
|
|||||||
.ToListAsyncLinqToDB();
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
var deleted = await uow.GetTable<Warning>()
|
var deleted = await uow.GetTable<Warning>()
|
||||||
.Where(x => toDelete.Contains(x.Id))
|
.Where(x => toDelete.Contains(x.Id))
|
||||||
.DeleteAsync();
|
.DeleteAsync();
|
||||||
|
|
||||||
if (cleared > 0 || deleted > 0)
|
if (cleared > 0 || deleted > 0)
|
||||||
{
|
{
|
||||||
@@ -324,11 +329,11 @@ public class UserPunishService : INService, IReadyExecutor
|
|||||||
await uow.SaveChangesAsync();
|
await uow.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<int> GetWarnExpire(ulong guildId)
|
public Task<(int, bool)> GetWarnExpire(ulong guildId)
|
||||||
{
|
{
|
||||||
using var uow = _db.GetDbContext();
|
using var uow = _db.GetDbContext();
|
||||||
var config = uow.GuildConfigsForId(guildId, set => set);
|
var config = uow.GuildConfigsForId(guildId, set => set);
|
||||||
return Task.FromResult(config.WarnExpireHours / 24);
|
return Task.FromResult((config.WarnExpireHours / 24, config.WarnExpireAction == WarnExpireAction.Delete));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task WarnExpireAsync(ulong guildId, int days, bool delete)
|
public async Task WarnExpireAsync(ulong guildId, int days, bool delete)
|
||||||
@@ -377,18 +382,19 @@ public class UserPunishService : INService, IReadyExecutor
|
|||||||
return toReturn;
|
return toReturn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool WarnPunish(
|
public async Task<bool> WarnPunish(
|
||||||
ulong guildId,
|
ulong guildId,
|
||||||
int number,
|
int number,
|
||||||
PunishmentAction punish,
|
PunishmentAction punish,
|
||||||
StoopidTime time,
|
TimeSpan? time,
|
||||||
IRole role = null)
|
IRole role = null)
|
||||||
{
|
{
|
||||||
// these 3 don't make sense with time
|
// these 3 don't make sense with time
|
||||||
if (punish is PunishmentAction.Softban or PunishmentAction.Kick or PunishmentAction.RemoveRoles
|
if (punish is PunishmentAction.Softban or PunishmentAction.Kick or PunishmentAction.RemoveRoles
|
||||||
&& time is not null)
|
&& time is not null)
|
||||||
return false;
|
return false;
|
||||||
if (number <= 0 || (time is not null && time.Time > TimeSpan.FromDays(49)))
|
|
||||||
|
if (number <= 0 || (time is not null && time > TimeSpan.FromDays(59)))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (punish is PunishmentAction.AddRole && role is null)
|
if (punish is PunishmentAction.AddRole && role is null)
|
||||||
@@ -397,47 +403,51 @@ public class UserPunishService : INService, IReadyExecutor
|
|||||||
if (punish is PunishmentAction.TimeOut && time is null)
|
if (punish is PunishmentAction.TimeOut && time is null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
using var uow = _db.GetDbContext();
|
var timeMinutes = (int?)time?.TotalMinutes ?? 0;
|
||||||
var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
|
var roleId = punish == PunishmentAction.AddRole ? role!.Id : default(ulong?);
|
||||||
var toDelete = ps.Where(x => x.Count == number);
|
await using var uow = _db.GetDbContext();
|
||||||
|
await uow.GetTable<WarningPunishment>()
|
||||||
uow.RemoveRange(toDelete);
|
.InsertOrUpdateAsync(() => new()
|
||||||
|
{
|
||||||
ps.Add(new()
|
GuildId = guildId,
|
||||||
{
|
Count = number,
|
||||||
Count = number,
|
Punishment = punish,
|
||||||
Punishment = punish,
|
Time = timeMinutes,
|
||||||
Time = (int?)time?.Time.TotalMinutes ?? 0,
|
RoleId = roleId
|
||||||
RoleId = punish == PunishmentAction.AddRole ? role!.Id : default(ulong?)
|
},
|
||||||
});
|
_ => new()
|
||||||
uow.SaveChanges();
|
{
|
||||||
|
Punishment = punish,
|
||||||
|
Time = timeMinutes,
|
||||||
|
RoleId = roleId
|
||||||
|
},
|
||||||
|
() => new()
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
Count = number
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool WarnPunishRemove(ulong guildId, int number)
|
public async Task<bool> WarnPunishRemove(ulong guildId, int count)
|
||||||
{
|
{
|
||||||
if (number <= 0)
|
await using var uow = _db.GetDbContext();
|
||||||
return false;
|
var numDeleted = await uow.GetTable<WarningPunishment>()
|
||||||
|
.DeleteAsync(x => x.GuildId == guildId && x.Count == count);
|
||||||
|
|
||||||
using var uow = _db.GetDbContext();
|
return numDeleted > 0;
|
||||||
var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
|
|
||||||
var p = ps.FirstOrDefault(x => x.Count == number);
|
|
||||||
|
|
||||||
if (p is not null)
|
|
||||||
{
|
|
||||||
uow.Remove(p);
|
|
||||||
uow.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public WarningPunishment[] WarnPunishList(ulong guildId)
|
|
||||||
|
public async Task<WarningPunishment[]> WarnPunishList(ulong guildId)
|
||||||
{
|
{
|
||||||
using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
return uow.GuildConfigsForId(guildId, gc => gc.Include(x => x.WarnPunishments))
|
|
||||||
.WarnPunishments.OrderBy(x => x.Count)
|
var wps = uow.GetTable<WarningPunishment>()
|
||||||
.ToArray();
|
.Where(x => x.GuildId == guildId)
|
||||||
|
.OrderBy(x => x.Count)
|
||||||
|
.ToArray();
|
||||||
|
return wps;
|
||||||
}
|
}
|
||||||
|
|
||||||
public (IReadOnlyCollection<(string Original, ulong? Id, string Reason)> Bans, int Missing) MassKill(
|
public (IReadOnlyCollection<(string Original, ulong? Id, string Reason)> Bans, int Missing) MassKill(
|
||||||
@@ -607,12 +617,12 @@ public class UserPunishService : INService, IReadyExecutor
|
|||||||
return await _repSvc.ReplaceAsync(output, repCtx);
|
return await _repSvc.ReplaceAsync(output, repCtx);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Warning> WarnDelete(ulong userId, int index)
|
public async Task<Warning> WarnDelete(ulong guildId, ulong userId, int index)
|
||||||
{
|
{
|
||||||
await using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
var warn = await uow.GetTable<Warning>()
|
var warn = await uow.GetTable<Warning>()
|
||||||
.Where(x => x.UserId == userId)
|
.Where(x => x.GuildId == guildId && x.UserId == userId)
|
||||||
.OrderByDescending(x => x.DateAdded)
|
.OrderByDescending(x => x.DateAdded)
|
||||||
.Skip(index)
|
.Skip(index)
|
||||||
.FirstOrDefaultAsyncLinqToDB();
|
.FirstOrDefaultAsyncLinqToDB();
|
||||||
@@ -626,4 +636,73 @@ public class UserPunishService : INService, IReadyExecutor
|
|||||||
|
|
||||||
return warn;
|
return warn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> WarnDelete(ulong guildId, int id)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
|
var numDeleted = await uow.GetTable<Warning>()
|
||||||
|
.Where(x => x.GuildId == guildId && x.Id == id)
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
return numDeleted > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(IReadOnlyCollection<Warning> latest, int totalCount)> GetLatestWarnings(
|
||||||
|
ulong guildId,
|
||||||
|
int page = 1)
|
||||||
|
{
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(page);
|
||||||
|
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
var latest = await uow.GetTable<Warning>()
|
||||||
|
.Where(x => x.GuildId == guildId)
|
||||||
|
.OrderByDescending(x => x.DateAdded)
|
||||||
|
.Skip(10 * (page - 1))
|
||||||
|
.Take(10)
|
||||||
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
|
var totalCount = await uow.GetTable<Warning>()
|
||||||
|
.Where(x => x.GuildId == guildId)
|
||||||
|
.CountAsyncLinqToDB();
|
||||||
|
|
||||||
|
return (latest, totalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ForgiveWarning(ulong requestGuildId, int warnId, string modName)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
var success = await uow.GetTable<Warning>()
|
||||||
|
.Where(x => x.GuildId == requestGuildId && x.Id == warnId)
|
||||||
|
.UpdateAsync(_ => new()
|
||||||
|
{
|
||||||
|
Forgiven = true,
|
||||||
|
ForgivenBy = modName,
|
||||||
|
})
|
||||||
|
== 1;
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(IReadOnlyCollection<Warning> latest, int totalCount)> GetUserWarnings(
|
||||||
|
ulong guildId,
|
||||||
|
ulong userId,
|
||||||
|
int page)
|
||||||
|
{
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(page);
|
||||||
|
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
var latest = await uow.GetTable<Warning>()
|
||||||
|
.Where(x => x.GuildId == guildId && x.UserId == userId)
|
||||||
|
.OrderByDescending(x => x.DateAdded)
|
||||||
|
.Skip(10 * (page - 1))
|
||||||
|
.Take(10)
|
||||||
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
|
var totalCount = await uow.GetTable<Warning>()
|
||||||
|
.Where(x => x.GuildId == guildId && x.UserId == userId)
|
||||||
|
.CountAsyncLinqToDB();
|
||||||
|
|
||||||
|
return (latest, totalCount);
|
||||||
|
}
|
||||||
}
|
}
|
@@ -6,48 +6,7 @@ namespace NadekoBot.Modules.NadekoExpressions;
|
|||||||
|
|
||||||
public static class NadekoExpressionExtensions
|
public static class NadekoExpressionExtensions
|
||||||
{
|
{
|
||||||
private static string ResolveTriggerString(this string str, DiscordSocketClient client)
|
|
||||||
=> str.Replace("%bot.mention%", client.CurrentUser.Mention, StringComparison.Ordinal);
|
|
||||||
|
|
||||||
public static async Task<IUserMessage> Send(
|
|
||||||
this NadekoExpression cr,
|
|
||||||
IUserMessage ctx,
|
|
||||||
IReplacementService repSvc,
|
|
||||||
DiscordSocketClient client,
|
|
||||||
IMessageSenderService sender)
|
|
||||||
{
|
|
||||||
var channel = cr.DmResponse ? await ctx.Author.CreateDMChannelAsync() : ctx.Channel;
|
|
||||||
|
|
||||||
var trigger = cr.Trigger.ResolveTriggerString(client);
|
|
||||||
var substringIndex = trigger.Length;
|
|
||||||
if (cr.ContainsAnywhere)
|
|
||||||
{
|
|
||||||
var pos = ctx.Content.AsSpan().GetWordPosition(trigger);
|
|
||||||
if (pos == WordPosition.Start)
|
|
||||||
substringIndex += 1;
|
|
||||||
else if (pos == WordPosition.End)
|
|
||||||
substringIndex = ctx.Content.Length;
|
|
||||||
else if (pos == WordPosition.Middle)
|
|
||||||
substringIndex += ctx.Content.IndexOf(trigger, StringComparison.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
var canMentionEveryone = (ctx.Author as IGuildUser)?.GuildPermissions.MentionEveryone ?? true;
|
|
||||||
|
|
||||||
var repCtx = new ReplacementContext(client: client,
|
|
||||||
guild: (ctx.Channel as ITextChannel)?.Guild as SocketGuild,
|
|
||||||
channel: ctx.Channel,
|
|
||||||
user: ctx.Author
|
|
||||||
)
|
|
||||||
.WithOverride("%target%",
|
|
||||||
() => canMentionEveryone
|
|
||||||
? ctx.Content[substringIndex..].Trim()
|
|
||||||
: ctx.Content[substringIndex..].Trim().SanitizeMentions(true));
|
|
||||||
|
|
||||||
var text = SmartText.CreateFrom(cr.Response);
|
|
||||||
text = await repSvc.ReplaceAsync(text, repCtx);
|
|
||||||
|
|
||||||
return await sender.Response(channel).Text(text).Sanitize(false).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static WordPosition GetWordPosition(this ReadOnlySpan<char> str, in ReadOnlySpan<char> word)
|
public static WordPosition GetWordPosition(this ReadOnlySpan<char> str, in ReadOnlySpan<char> word)
|
||||||
|
@@ -12,10 +12,10 @@ public partial class NadekoExpressions : NadekoModule<NadekoExpressionsService>
|
|||||||
All
|
All
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
private readonly IHttpClientFactory _clientFactory;
|
private readonly IHttpClientFactory _clientFactory;
|
||||||
|
|
||||||
public NadekoExpressions(IBotCredentials creds, IHttpClientFactory clientFactory)
|
public NadekoExpressions(IBotCreds creds, IHttpClientFactory clientFactory)
|
||||||
{
|
{
|
||||||
_creds = creds;
|
_creds = creds;
|
||||||
_clientFactory = clientFactory;
|
_clientFactory = clientFactory;
|
||||||
|
@@ -67,7 +67,6 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
|
|||||||
// private readonly GlobalPermissionService _gperm;
|
// private readonly GlobalPermissionService _gperm;
|
||||||
// private readonly CmdCdService _cmdCds;
|
// private readonly CmdCdService _cmdCds;
|
||||||
private readonly IPermissionChecker _permChecker;
|
private readonly IPermissionChecker _permChecker;
|
||||||
private readonly ICommandHandler _cmd;
|
|
||||||
private readonly IBotStrings _strings;
|
private readonly IBotStrings _strings;
|
||||||
private readonly IBot _bot;
|
private readonly IBot _bot;
|
||||||
private readonly IPubSub _pubSub;
|
private readonly IPubSub _pubSub;
|
||||||
@@ -84,7 +83,6 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
|
|||||||
IBotStrings strings,
|
IBotStrings strings,
|
||||||
IBot bot,
|
IBot bot,
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
ICommandHandler cmd,
|
|
||||||
IPubSub pubSub,
|
IPubSub pubSub,
|
||||||
IMessageSenderService sender,
|
IMessageSenderService sender,
|
||||||
IReplacementService repSvc,
|
IReplacementService repSvc,
|
||||||
@@ -93,7 +91,6 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
|
|||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_client = client;
|
_client = client;
|
||||||
_cmd = cmd;
|
|
||||||
_strings = strings;
|
_strings = strings;
|
||||||
_bot = bot;
|
_bot = bot;
|
||||||
_pubSub = pubSub;
|
_pubSub = pubSub;
|
||||||
@@ -249,46 +246,54 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (guild is SocketGuild sg)
|
if (guild is not SocketGuild sg)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var result = await _permChecker.CheckPermsAsync(
|
||||||
|
guild,
|
||||||
|
msg.Channel,
|
||||||
|
msg.Author,
|
||||||
|
"ACTUALEXPRESSIONS",
|
||||||
|
expr.Trigger
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.IsAllowed)
|
||||||
{
|
{
|
||||||
var result = await _permChecker.CheckPermsAsync(
|
var cache = _pc.GetCacheFor(guild.Id);
|
||||||
guild,
|
if (cache.Verbose)
|
||||||
msg.Channel,
|
|
||||||
msg.Author,
|
|
||||||
"ACTUALEXPRESSIONS",
|
|
||||||
expr.Trigger
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!result.IsAllowed)
|
|
||||||
{
|
{
|
||||||
var cache = _pc.GetCacheFor(guild.Id);
|
if (result.TryPickT3(out var disallowed, out _))
|
||||||
if (cache.Verbose)
|
|
||||||
{
|
{
|
||||||
if (result.TryPickT3(out var disallowed, out _))
|
var permissionMessage = _strings.GetText(strs.perm_prevent(disallowed.PermIndex + 1,
|
||||||
|
Format.Bold(disallowed.PermText)),
|
||||||
|
sg.Id);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _sender.Response(msg.Channel)
|
||||||
|
.Error(permissionMessage)
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
{
|
{
|
||||||
var permissionMessage = _strings.GetText(strs.perm_prevent(disallowed.PermIndex + 1,
|
|
||||||
Format.Bold(disallowed.PermText)),
|
|
||||||
sg.Id);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _sender.Response(msg.Channel)
|
|
||||||
.Error(permissionMessage)
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Information("{PermissionMessage}", permissionMessage);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
Log.Information("{PermissionMessage}", permissionMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sentMsg = await expr.Send(msg, _repSvc, _client, _sender);
|
var cu = sg.CurrentUser;
|
||||||
|
|
||||||
|
var channel = expr.DmResponse ? await msg.Author.CreateDMChannelAsync() : msg.Channel;
|
||||||
|
|
||||||
|
// have no perms to speak in that channel
|
||||||
|
if (channel is ITextChannel tc && !cu.GetPermissions(tc).SendMessages)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var sentMsg = await Send(expr, msg, channel);
|
||||||
|
|
||||||
var reactions = expr.GetReactions();
|
var reactions = expr.GetReactions();
|
||||||
foreach (var reaction in reactions)
|
foreach (var reaction in reactions)
|
||||||
@@ -336,6 +341,47 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public string ResolveTriggerString(string str)
|
||||||
|
=> str.Replace("%bot.mention%", _client.CurrentUser.Mention, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
public async Task<IUserMessage> Send(
|
||||||
|
NadekoExpression cr,
|
||||||
|
IUserMessage ctx,
|
||||||
|
IMessageChannel channel
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var trigger = ResolveTriggerString(cr.Trigger);
|
||||||
|
var substringIndex = trigger.Length;
|
||||||
|
if (cr.ContainsAnywhere)
|
||||||
|
{
|
||||||
|
var pos = ctx.Content.AsSpan().GetWordPosition(trigger);
|
||||||
|
if (pos == WordPosition.Start)
|
||||||
|
substringIndex += 1;
|
||||||
|
else if (pos == WordPosition.End)
|
||||||
|
substringIndex = ctx.Content.Length;
|
||||||
|
else if (pos == WordPosition.Middle)
|
||||||
|
substringIndex += ctx.Content.IndexOf(trigger, StringComparison.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
var canMentionEveryone = (ctx.Author as IGuildUser)?.GuildPermissions.MentionEveryone ?? true;
|
||||||
|
|
||||||
|
var repCtx = new ReplacementContext(client: _client,
|
||||||
|
guild: (ctx.Channel as ITextChannel)?.Guild as SocketGuild,
|
||||||
|
channel: ctx.Channel,
|
||||||
|
user: ctx.Author
|
||||||
|
)
|
||||||
|
.WithOverride("%target%",
|
||||||
|
() => canMentionEveryone
|
||||||
|
? ctx.Content[substringIndex..].Trim()
|
||||||
|
: ctx.Content[substringIndex..].Trim().SanitizeMentions(true));
|
||||||
|
|
||||||
|
var text = SmartText.CreateFrom(cr.Response);
|
||||||
|
text = await _repSvc.ReplaceAsync(text, repCtx);
|
||||||
|
|
||||||
|
return await _sender.Response(channel).Text(text).Sanitize(false).SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ResetExprReactions(ulong? maybeGuildId, int id)
|
public async Task ResetExprReactions(ulong? maybeGuildId, int id)
|
||||||
{
|
{
|
||||||
NadekoExpression expr;
|
NadekoExpression expr;
|
||||||
@@ -789,7 +835,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
|
|||||||
|
|
||||||
if (newguildExpressions.TryGetValue(guildId, out var exprs))
|
if (newguildExpressions.TryGetValue(guildId, out var exprs))
|
||||||
{
|
{
|
||||||
return (exprs.Where(x => x.Trigger.Contains(query))
|
return (exprs.Where(x => x.Trigger.Contains(query) || x.Response.Contains(query))
|
||||||
.Skip(page * 9)
|
.Skip(page * 9)
|
||||||
.Take(9)
|
.Take(9)
|
||||||
.ToArray(), exprs.Length);
|
.ToArray(), exprs.Length);
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.EntityFrameworkCore;
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using LinqToDB.Tools;
|
||||||
using NadekoBot.Db.Models;
|
using NadekoBot.Db.Models;
|
||||||
using NadekoBot.Modules.Gambling.Bank;
|
using NadekoBot.Modules.Gambling.Bank;
|
||||||
using NadekoBot.Modules.Gambling.Common;
|
using NadekoBot.Modules.Gambling.Common;
|
||||||
@@ -127,7 +128,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
customId: "timely:remind_me"),
|
customId: "timely:remind_me"),
|
||||||
(smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromHours(period)))
|
(smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromHours(period)))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Creates timely reminder button, parameter in milliseconds.
|
// Creates timely reminder button, parameter in milliseconds.
|
||||||
private NadekoInteractionBase CreateRemindMeInteraction(double ms)
|
private NadekoInteractionBase CreateRemindMeInteraction(double ms)
|
||||||
=> _inter
|
=> _inter
|
||||||
@@ -166,7 +167,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(interaction).SendAsync();
|
await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(interaction).SendAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var patron = await _ps.GetPatronAsync(ctx.User.Id);
|
var patron = await _ps.GetPatronAsync(ctx.User.Id);
|
||||||
|
|
||||||
@@ -625,8 +626,6 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
|
|
||||||
var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
|
var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
|
||||||
|
|
||||||
// List<DiscordUser> cleanRichest;
|
|
||||||
// it's pointless to have clean on dm context
|
|
||||||
if (ctx.Guild is null)
|
if (ctx.Guild is null)
|
||||||
{
|
{
|
||||||
opts.Clean = false;
|
opts.Clean = false;
|
||||||
@@ -640,13 +639,18 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
await ctx.Channel.TriggerTypingAsync();
|
await ctx.Channel.TriggerTypingAsync();
|
||||||
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
|
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
|
||||||
|
|
||||||
|
var users = ((SocketGuild)ctx.Guild).Users.Map(x => x.Id);
|
||||||
|
var perPage = 9;
|
||||||
|
|
||||||
await using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
|
var cleanRichest = await uow.GetTable<DiscordUser>()
|
||||||
|
.Where(x => x.UserId.In(users))
|
||||||
|
.OrderByDescending(x => x.CurrencyAmount)
|
||||||
|
.Skip(curPage * perPage)
|
||||||
|
.Take(perPage)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
var cleanRichest = await uow.Set<DiscordUser>()
|
return cleanRichest;
|
||||||
.GetTopRichest(_client.CurrentUser.Id, 0, 1000);
|
|
||||||
|
|
||||||
var sg = (SocketGuild)ctx.Guild!;
|
|
||||||
return cleanRichest.Where(x => sg.GetUser(x.UserId) is not null).ToList();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -655,13 +659,9 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var res = Response()
|
|
||||||
.Paginated();
|
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Paginated()
|
.Paginated()
|
||||||
.PageItems(GetTopRichest)
|
.PageItems(GetTopRichest)
|
||||||
.TotalElements(900)
|
|
||||||
.PageSize(9)
|
.PageSize(9)
|
||||||
.CurrentPage(page)
|
.CurrentPage(page)
|
||||||
.Page((toSend, curPage) =>
|
.Page((toSend, curPage) =>
|
||||||
|
@@ -14,13 +14,13 @@ public class VoteModel
|
|||||||
public class VoteRewardService : INService, IReadyExecutor
|
public class VoteRewardService : INService, IReadyExecutor
|
||||||
{
|
{
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
private readonly ICurrencyService _currencyService;
|
private readonly ICurrencyService _currencyService;
|
||||||
private readonly GamblingConfigService _gamb;
|
private readonly GamblingConfigService _gamb;
|
||||||
|
|
||||||
public VoteRewardService(
|
public VoteRewardService(
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
IBotCredentials creds,
|
IBotCreds creds,
|
||||||
ICurrencyService currencyService,
|
ICurrencyService currencyService,
|
||||||
GamblingConfigService gamb)
|
GamblingConfigService gamb)
|
||||||
{
|
{
|
||||||
|
@@ -227,7 +227,7 @@ public partial class Gambling
|
|||||||
if (page > 100)
|
if (page > 100)
|
||||||
page = 100;
|
page = 100;
|
||||||
|
|
||||||
var waifus = _service.GetTopWaifusAtPage(page).ToList();
|
var waifus = await _service.GetTopWaifusAtPage(page);
|
||||||
|
|
||||||
if (waifus.Count == 0)
|
if (waifus.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -252,18 +252,18 @@ public partial class Gambling
|
|||||||
var claimer = "no one";
|
var claimer = "no one";
|
||||||
string status;
|
string status;
|
||||||
|
|
||||||
var waifuUsername = w.Username.TrimTo(20);
|
var waifuUsername = w.WaifuName.TrimTo(20);
|
||||||
var claimerUsername = w.Claimer?.TrimTo(20);
|
var claimerUsername = w.ClaimerName?.TrimTo(20);
|
||||||
|
|
||||||
if (w.Claimer is not null)
|
if (w.ClaimerName is not null)
|
||||||
claimer = $"{claimerUsername}#{w.ClaimerDiscrim}";
|
claimer = $"{claimerUsername}";
|
||||||
if (w.Affinity is null)
|
if (w.Affinity is null)
|
||||||
status = $"... but {waifuUsername}'s heart is empty";
|
status = $"... but {waifuUsername}'s heart is empty";
|
||||||
else if (w.Affinity + w.AffinityDiscrim == w.Claimer + w.ClaimerDiscrim)
|
else if (w.Affinity == w.ClaimerName)
|
||||||
status = $"... and {waifuUsername} likes {claimerUsername} too <3";
|
status = $"... and {waifuUsername} likes {claimerUsername} too <3";
|
||||||
else
|
else
|
||||||
status = $"... but {waifuUsername}'s heart belongs to {w.Affinity.TrimTo(20)}#{w.AffinityDiscrim}";
|
status = $"... but {waifuUsername}'s heart belongs to {w.Affinity.TrimTo(20)}";
|
||||||
return $"**{waifuUsername}#{w.Discrim}** - claimed by **{claimer}**\n\t{status}";
|
return $"**{waifuUsername}** - claimed by **{claimer}**\n\t{status}";
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@@ -15,7 +15,7 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
private readonly ICurrencyService _cs;
|
private readonly ICurrencyService _cs;
|
||||||
private readonly IBotCache _cache;
|
private readonly IBotCache _cache;
|
||||||
private readonly GamblingConfigService _gss;
|
private readonly GamblingConfigService _gss;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
|
|
||||||
public WaifuService(
|
public WaifuService(
|
||||||
@@ -23,7 +23,7 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
ICurrencyService cs,
|
ICurrencyService cs,
|
||||||
IBotCache cache,
|
IBotCache cache,
|
||||||
GamblingConfigService gss,
|
GamblingConfigService gss,
|
||||||
IBotCredentials creds,
|
IBotCreds creds,
|
||||||
DiscordSocketClient client)
|
DiscordSocketClient client)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
@@ -300,10 +300,10 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
return (oldAff, success, remaining);
|
return (oldAff, success, remaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<WaifuLbResult> GetTopWaifusAtPage(int page, int perPage = 9)
|
public async Task<IReadOnlyList<WaifuLbResult>> GetTopWaifusAtPage(int page, int perPage = 9)
|
||||||
{
|
{
|
||||||
using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
return uow.Set<WaifuInfo>().GetTop(perPage, page * perPage);
|
return await uow.Set<WaifuInfo>().GetTop(perPage, page * perPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ulong GetWaifuUserId(ulong ownerId, string name)
|
public ulong GetWaifuUserId(ulong ownerId, string name)
|
||||||
@@ -577,7 +577,7 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
{
|
{
|
||||||
await using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
await uow.GetTable<WaifuInfo>()
|
await uow.GetTable<WaifuInfo>()
|
||||||
.Where(x => x.Price > minPrice && x.ClaimerId == null)
|
.Where(x => x.Price > minPrice && x.ClaimerId != null)
|
||||||
.UpdateAsync(old => new()
|
.UpdateAsync(old => new()
|
||||||
{
|
{
|
||||||
Price = (long)(old.Price * claimedMulti)
|
Price = (long)(old.Price * claimedMulti)
|
||||||
|
@@ -25,30 +25,35 @@ public static class WaifuExtensions
|
|||||||
return includes(waifus).AsQueryable().FirstOrDefault(wi => wi.Waifu.UserId == userId);
|
return includes(waifus).AsQueryable().FirstOrDefault(wi => wi.Waifu.UserId == userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<WaifuLbResult> GetTop(this DbSet<WaifuInfo> waifus, int count, int skip = 0)
|
public static async Task<IReadOnlyList<WaifuLbResult>> GetTop(this DbSet<WaifuInfo> waifus, int count, int skip = 0)
|
||||||
{
|
{
|
||||||
ArgumentOutOfRangeException.ThrowIfNegative(count);
|
ArgumentOutOfRangeException.ThrowIfNegative(count);
|
||||||
|
|
||||||
if (count == 0)
|
if (count == 0)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
return waifus.Include(wi => wi.Waifu)
|
return await waifus.Include(wi => wi.Waifu)
|
||||||
.Include(wi => wi.Affinity)
|
.Include(wi => wi.Affinity)
|
||||||
.Include(wi => wi.Claimer)
|
.Include(wi => wi.Claimer)
|
||||||
.OrderByDescending(wi => wi.Price)
|
.OrderByDescending(wi => wi.Price)
|
||||||
.Skip(skip)
|
.Skip(skip)
|
||||||
.Take(count)
|
.Take(count)
|
||||||
.Select(x => new WaifuLbResult
|
.Select(x => new WaifuLbResult
|
||||||
{
|
{
|
||||||
Affinity = x.Affinity == null ? null : x.Affinity.Username,
|
Affinity = x.Affinity == null
|
||||||
AffinityDiscrim = x.Affinity == null ? null : x.Affinity.Discriminator,
|
? null
|
||||||
Claimer = x.Claimer == null ? null : x.Claimer.Username,
|
: x.Affinity.Username
|
||||||
ClaimerDiscrim = x.Claimer == null ? null : x.Claimer.Discriminator,
|
+ (x.Affinity.Discriminator != "0000" ? "#" + x.Affinity.Discriminator : ""),
|
||||||
Username = x.Waifu.Username,
|
ClaimerName =
|
||||||
Discrim = x.Waifu.Discriminator,
|
x.Claimer == null
|
||||||
Price = x.Price
|
? null
|
||||||
})
|
: x.Claimer.Username
|
||||||
.ToList();
|
+ (x.Claimer.Discriminator != "0000" ? "#" + x.Claimer.Discriminator : ""),
|
||||||
|
WaifuName = x.Waifu.Username
|
||||||
|
+ (x.Waifu.Discriminator != "0000" ? "#" + x.Waifu.Discriminator : ""),
|
||||||
|
Price = x.Price
|
||||||
|
})
|
||||||
|
.ToListAsyncEF();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static decimal GetTotalValue(this DbSet<WaifuInfo> waifus)
|
public static decimal GetTotalValue(this DbSet<WaifuInfo> waifus)
|
||||||
@@ -64,7 +69,7 @@ public static class WaifuExtensions
|
|||||||
public static async Task<WaifuInfoStats> GetWaifuInfoAsync(this DbContext ctx, ulong userId)
|
public static async Task<WaifuInfoStats> GetWaifuInfoAsync(this DbContext ctx, ulong userId)
|
||||||
{
|
{
|
||||||
await ctx.EnsureUserCreatedAsync(userId);
|
await ctx.EnsureUserCreatedAsync(userId);
|
||||||
|
|
||||||
await ctx.Set<WaifuInfo>()
|
await ctx.Set<WaifuInfo>()
|
||||||
.ToLinqToDBTable()
|
.ToLinqToDBTable()
|
||||||
.InsertOrUpdateAsync(() => new()
|
.InsertOrUpdateAsync(() => new()
|
||||||
|
@@ -3,14 +3,11 @@ namespace NadekoBot.Db.Models;
|
|||||||
|
|
||||||
public class WaifuLbResult
|
public class WaifuLbResult
|
||||||
{
|
{
|
||||||
public string Username { get; set; }
|
public string WaifuName { get; set; }
|
||||||
public string Discrim { get; set; }
|
|
||||||
|
|
||||||
public string Claimer { get; set; }
|
public string ClaimerName { get; set; }
|
||||||
public string ClaimerDiscrim { get; set; }
|
|
||||||
|
|
||||||
public string Affinity { get; set; }
|
public string Affinity { get; set; }
|
||||||
public string AffinityDiscrim { get; set; }
|
|
||||||
|
|
||||||
public long Price { get; set; }
|
public long Price { get; set; }
|
||||||
}
|
}
|
@@ -19,7 +19,7 @@ public class ChatterBotService : IExecOnMessage
|
|||||||
|
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly IPermissionChecker _perms;
|
private readonly IPermissionChecker _perms;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
private readonly GamesConfigService _gcs;
|
private readonly GamesConfigService _gcs;
|
||||||
private readonly IMessageSenderService _sender;
|
private readonly IMessageSenderService _sender;
|
||||||
@@ -32,7 +32,7 @@ public class ChatterBotService : IExecOnMessage
|
|||||||
IBot bot,
|
IBot bot,
|
||||||
IPatronageService ps,
|
IPatronageService ps,
|
||||||
IHttpClientFactory factory,
|
IHttpClientFactory factory,
|
||||||
IBotCredentials creds,
|
IBotCreds creds,
|
||||||
GamesConfigService gcs,
|
GamesConfigService gcs,
|
||||||
IMessageSenderService sender,
|
IMessageSenderService sender,
|
||||||
DbService db)
|
DbService db)
|
||||||
|
24
src/NadekoBot/Modules/Games/NCanvas/INCanvasService.cs
Normal file
24
src/NadekoBot/Modules/Games/NCanvas/INCanvasService.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using NadekoBot.Db.Models;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public interface INCanvasService
|
||||||
|
{
|
||||||
|
Task<uint[]> GetCanvas();
|
||||||
|
Task<NCPixel[]> GetPixelGroup(int position);
|
||||||
|
|
||||||
|
Task<SetPixelResult> SetPixel(
|
||||||
|
int position,
|
||||||
|
uint color,
|
||||||
|
string text,
|
||||||
|
ulong userId,
|
||||||
|
long price);
|
||||||
|
|
||||||
|
Task<bool> SetImage(uint[] img);
|
||||||
|
|
||||||
|
Task<NCPixel?> GetPixel(int x, int y);
|
||||||
|
Task<NCPixel?> GetPixel(int position);
|
||||||
|
int GetHeight();
|
||||||
|
int GetWidth();
|
||||||
|
Task ResetAsync();
|
||||||
|
}
|
289
src/NadekoBot/Modules/Games/NCanvas/NCanvasCommands.cs
Normal file
289
src/NadekoBot/Modules/Games/NCanvas/NCanvasCommands.cs
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
|
using SixLabors.Fonts;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Advanced;
|
||||||
|
using SixLabors.ImageSharp.Drawing.Processing;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using Image = SixLabors.ImageSharp.Image;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public partial class Games
|
||||||
|
{
|
||||||
|
public sealed class NCanvasCommands : NadekoModule
|
||||||
|
{
|
||||||
|
private readonly INCanvasService _service;
|
||||||
|
private readonly IHttpClientFactory _http;
|
||||||
|
private readonly FontProvider _fonts;
|
||||||
|
private readonly GamblingConfigService _gcs;
|
||||||
|
|
||||||
|
public NCanvasCommands(
|
||||||
|
INCanvasService service,
|
||||||
|
IHttpClientFactory http,
|
||||||
|
FontProvider fonts,
|
||||||
|
GamblingConfigService gcs)
|
||||||
|
{
|
||||||
|
_service = service;
|
||||||
|
_http = http;
|
||||||
|
_fonts = fonts;
|
||||||
|
_gcs = gcs;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task NCanvas()
|
||||||
|
{
|
||||||
|
var pixels = await _service.GetCanvas();
|
||||||
|
var image = new Image<Rgba32>(_service.GetWidth(), _service.GetHeight());
|
||||||
|
|
||||||
|
Parallel.For(0,
|
||||||
|
image.Height,
|
||||||
|
y =>
|
||||||
|
{
|
||||||
|
var pixelAccessor = image.DangerousGetPixelRowMemory(y);
|
||||||
|
var row = pixelAccessor.Span;
|
||||||
|
for (int x = 0; x < image.Width; x++)
|
||||||
|
{
|
||||||
|
row[x] = new Rgba32(pixels[(y * image.Width) + x]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await using var stream = await image.ToStreamAsync();
|
||||||
|
|
||||||
|
var hint = GetText(strs.nc_hint(prefix, _service.GetWidth(), _service.GetHeight()));
|
||||||
|
await Response()
|
||||||
|
.File(stream, "ncanvas.png")
|
||||||
|
.Embed(_sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
#if GLOBAL_NADEKO
|
||||||
|
.WithDescription("https://dashy.nadeko.bot/ncanvas")
|
||||||
|
#endif
|
||||||
|
.WithFooter(hint)
|
||||||
|
.WithImageUrl("attachment://ncanvas.png"))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public Task NCzoom(int row, int col)
|
||||||
|
=> NCzoom((col * _service.GetWidth()) + row);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task NCzoom(kwum position)
|
||||||
|
{
|
||||||
|
var w = _service.GetWidth();
|
||||||
|
var h = _service.GetHeight();
|
||||||
|
|
||||||
|
if (position < 0 || position >= w * h)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.invalid_input).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var img = await GetZoomImage(position);
|
||||||
|
await using var stream = await img.ToStreamAsync();
|
||||||
|
await ctx.Channel.SendFileAsync(stream, $"zoom_{position}.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Image<Rgba32>> GetZoomImage(kwum position)
|
||||||
|
{
|
||||||
|
var w = _service.GetWidth();
|
||||||
|
var pixels = await _service.GetPixelGroup(position);
|
||||||
|
|
||||||
|
var origX = ((position % w) - 2) * 100;
|
||||||
|
var origY = ((position / w) - 2) * 100;
|
||||||
|
|
||||||
|
var image = new Image<Rgba32>(500, 500);
|
||||||
|
|
||||||
|
const float fontSize = 30;
|
||||||
|
|
||||||
|
var posFont = _fonts.NotoSans.CreateFont(fontSize, FontStyle.Bold);
|
||||||
|
var size = TextMeasurer.MeasureSize("wwww", new TextOptions(posFont));
|
||||||
|
var scale = 100f / size.Width;
|
||||||
|
if (scale < 1)
|
||||||
|
posFont = _fonts.NotoSans.CreateFont(fontSize * scale, FontStyle.Bold);
|
||||||
|
var outlinePen = new SolidPen(SixLabors.ImageSharp.Color.Black, 1f);
|
||||||
|
|
||||||
|
Parallel.For(0,
|
||||||
|
pixels.Length,
|
||||||
|
i =>
|
||||||
|
{
|
||||||
|
var pix = pixels[i];
|
||||||
|
var startX = pix.Position % w * 100 - origX;
|
||||||
|
var startY = pix.Position / w * 100 - origY;
|
||||||
|
|
||||||
|
var color = new Rgba32(pix.Color);
|
||||||
|
image.Mutate(x => FillRectangleExtensions.Fill(x,
|
||||||
|
new SolidBrush(color),
|
||||||
|
new RectangleF(startX, startY, 100, 100)));
|
||||||
|
|
||||||
|
image.Mutate(x =>
|
||||||
|
{
|
||||||
|
x.DrawText(new RichTextOptions(posFont)
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Origin = new(startX + 50, startY + 50)
|
||||||
|
},
|
||||||
|
((kwum)pix.Position).ToString().PadLeft(2, '2'),
|
||||||
|
Brushes.Solid(SixLabors.ImageSharp.Color.White),
|
||||||
|
outlinePen);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// write the position on each section of the image
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task NcSetPixel(kwum position, string colorHex, [Leftover] string text = "")
|
||||||
|
{
|
||||||
|
if (position < 0 || position >= _service.GetWidth() * _service.GetHeight())
|
||||||
|
{
|
||||||
|
await Response().Error(strs.invalid_input).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorHex.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||||
|
colorHex = colorHex[2..];
|
||||||
|
|
||||||
|
if (!Rgba32.TryParseHex(colorHex, out var clr))
|
||||||
|
{
|
||||||
|
await Response().Error(strs.invalid_color).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pixel = await _service.GetPixel(position);
|
||||||
|
if (pixel is null)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.nc_pixel_not_found).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var prompt = GetText(strs.nc_pixel_set_confirm(Format.Code(position.ToString()),
|
||||||
|
Format.Bold(CurrencyHelper.N(pixel.Price,
|
||||||
|
Culture,
|
||||||
|
_gcs.Data.Currency.Sign))));
|
||||||
|
|
||||||
|
if (!await PromptUserConfirmAsync(_sender.CreateEmbed()
|
||||||
|
.WithPendingColor()
|
||||||
|
.WithDescription(prompt)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _service.SetPixel(position, clr.PackedValue, text, ctx.User.Id, pixel.Price);
|
||||||
|
|
||||||
|
using var img = await GetZoomImage(position);
|
||||||
|
await using var stream = await img.ToStreamAsync();
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Embed(_sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithDescription(GetText(strs.nc_pixel_set(Format.Code(position.ToString()))))
|
||||||
|
.WithImageUrl($"attachment://zoom_{position}.png"))
|
||||||
|
.File(stream, $"zoom_{position}.png")
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task NcPixel(int x, int y)
|
||||||
|
=> await NcPixel((y * _service.GetWidth()) + x);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task NcPixel(kwum position)
|
||||||
|
{
|
||||||
|
if (position < 0 || position >= _service.GetWidth() * _service.GetHeight())
|
||||||
|
{
|
||||||
|
await Response().Error(strs.invalid_input).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pixel = await _service.GetPixel(position);
|
||||||
|
if (pixel is null)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.nc_pixel_not_found).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var image = new Image<Rgba32>(100, 100);
|
||||||
|
image.Mutate(x
|
||||||
|
=> x.Fill(new SolidBrush(new Rgba32(pixel.Color)),
|
||||||
|
new RectangleF(0, 0, 100, 100)));
|
||||||
|
|
||||||
|
await using var stream = await image.ToStreamAsync();
|
||||||
|
|
||||||
|
var pos = new kwum(pixel.Position);
|
||||||
|
await Response()
|
||||||
|
.File(stream, $"{pixel.Position}.png")
|
||||||
|
.Embed(_sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithDescription(string.IsNullOrWhiteSpace(pixel.Text) ? string.Empty : pixel.Text)
|
||||||
|
.WithTitle(GetText(strs.nc_pixel(pos)))
|
||||||
|
.AddField(GetText(strs.nc_position),
|
||||||
|
$"{pixel.Position % _service.GetWidth()} {pixel.Position / _service.GetWidth()}",
|
||||||
|
true)
|
||||||
|
.AddField(GetText(strs.price), pixel.Price.ToString(), true)
|
||||||
|
.AddField(GetText(strs.color), "#" + new Rgba32(pixel.Color).ToHex())
|
||||||
|
.WithImageUrl($"attachment://{pixel.Position}.png"))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task NcSetImg()
|
||||||
|
{
|
||||||
|
var attach = ctx.Message.Attachments.FirstOrDefault();
|
||||||
|
if (attach is null)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.no_attach_found).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var w = _service.GetWidth();
|
||||||
|
var h = _service.GetHeight();
|
||||||
|
if (attach.Width != w || attach.Height != h)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.invalid_img_size(w, h)).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await PromptUserConfirmAsync(_sender.CreateEmbed()
|
||||||
|
.WithDescription(
|
||||||
|
"This will reset the canvas to the specified image. All prices, text and colors will be reset.\n\n"
|
||||||
|
+ "Are you sure you want to continue?")))
|
||||||
|
return;
|
||||||
|
|
||||||
|
using var http = _http.CreateClient();
|
||||||
|
await using var stream = await http.GetStreamAsync(attach.Url);
|
||||||
|
using var img = await Image.LoadAsync<Rgba32>(stream);
|
||||||
|
|
||||||
|
var pixels = new uint[_service.GetWidth() * _service.GetHeight()];
|
||||||
|
|
||||||
|
Parallel.For(0,
|
||||||
|
_service.GetWidth() * _service.GetHeight(),
|
||||||
|
i => pixels[i] = img[i % _service.GetWidth(), i / _service.GetWidth()].PackedValue);
|
||||||
|
|
||||||
|
// for (var y = 0; y < _service.GetHeight(); y++)
|
||||||
|
// for (var x = 0; x < _service.GetWidth(); x++)
|
||||||
|
// pixels[(y * _service.GetWidth()) + x] = img[x, y].PackedValue;
|
||||||
|
|
||||||
|
await _service.SetImage(pixels);
|
||||||
|
await ctx.OkAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task NcReset()
|
||||||
|
{
|
||||||
|
await _service.ResetAsync();
|
||||||
|
|
||||||
|
if (!await PromptUserConfirmAsync(_sender.CreateEmbed()
|
||||||
|
.WithDescription(
|
||||||
|
"This will delete all pixels and reset the canvas.\n\n"
|
||||||
|
+ "Are you sure you want to continue?")))
|
||||||
|
return;
|
||||||
|
|
||||||
|
await ctx.OkAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
206
src/NadekoBot/Modules/Games/NCanvas/NCanvasService.cs
Normal file
206
src/NadekoBot/Modules/Games/NCanvas/NCanvasService.cs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.Data;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
|
using NadekoBot.Db.Models;
|
||||||
|
using SixLabors.ImageSharp.ColorSpaces;
|
||||||
|
using SixLabors.ImageSharp.ColorSpaces.Conversion;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public sealed class NCanvasService : INCanvasService, IReadyExecutor, INService
|
||||||
|
{
|
||||||
|
private readonly TypedKey<uint[]> _canvasKey = new("ncanvas");
|
||||||
|
|
||||||
|
private readonly DbService _db;
|
||||||
|
private readonly IBotCache _cache;
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
private readonly ICurrencyService _cs;
|
||||||
|
|
||||||
|
public const int CANVAS_WIDTH = 500;
|
||||||
|
public const int CANVAS_HEIGHT = 350;
|
||||||
|
public const int INITIAL_PRICE = 3;
|
||||||
|
|
||||||
|
public NCanvasService(
|
||||||
|
DbService db,
|
||||||
|
IBotCache cache,
|
||||||
|
DiscordSocketClient client,
|
||||||
|
ICurrencyService cs)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_cache = cache;
|
||||||
|
_client = client;
|
||||||
|
_cs = cs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnReadyAsync()
|
||||||
|
{
|
||||||
|
if (_client.ShardId != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
|
if (await uow.GetTable<NCPixel>().CountAsyncLinqToDB() > 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await ResetAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ResetAsync()
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
await uow.GetTable<NCPixel>().DeleteAsync();
|
||||||
|
|
||||||
|
var toAdd = new List<int>();
|
||||||
|
for (var i = 0; i < CANVAS_WIDTH * CANVAS_HEIGHT; i++)
|
||||||
|
{
|
||||||
|
toAdd.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
await uow.GetTable<NCPixel>()
|
||||||
|
.BulkCopyAsync(toAdd.Select(x =>
|
||||||
|
{
|
||||||
|
var clr = ColorSpaceConverter.ToRgb(new Hsv(((float)Random.Shared.NextDouble() * 360),
|
||||||
|
(float)(0.5 + (Random.Shared.NextDouble() * 0.49)),
|
||||||
|
(float)(0.4 + (Random.Shared.NextDouble() / 5 + (x % 100 * 0.2)))))
|
||||||
|
.ToVector3();
|
||||||
|
|
||||||
|
var packed = new Rgba32(clr).PackedValue;
|
||||||
|
return new NCPixel()
|
||||||
|
{
|
||||||
|
Color = packed,
|
||||||
|
Price = 1,
|
||||||
|
Position = x,
|
||||||
|
Text = "",
|
||||||
|
OwnerId = 0
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task<uint[]> InternalGetCanvas()
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
var colors = await uow.GetTable<NCPixel>()
|
||||||
|
.OrderBy(x => x.Position)
|
||||||
|
.Select(x => x.Color)
|
||||||
|
.ToArrayAsyncLinqToDB();
|
||||||
|
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<uint[]> GetCanvas()
|
||||||
|
{
|
||||||
|
return await _cache.GetOrAddAsync(_canvasKey,
|
||||||
|
async () => await InternalGetCanvas(),
|
||||||
|
TimeSpan.FromSeconds(15))
|
||||||
|
?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SetPixelResult> SetPixel(
|
||||||
|
int position,
|
||||||
|
uint color,
|
||||||
|
string text,
|
||||||
|
ulong userId,
|
||||||
|
long price)
|
||||||
|
{
|
||||||
|
if (position < 0 || position >= CANVAS_WIDTH * CANVAS_HEIGHT)
|
||||||
|
return SetPixelResult.InvalidInput;
|
||||||
|
|
||||||
|
var wallet = await _cs.GetWalletAsync(userId);
|
||||||
|
|
||||||
|
var paid = await wallet.Take(price, new("canvas", "pixel-buy", $"Bought pixel {new kwum(position)}"));
|
||||||
|
if (!paid)
|
||||||
|
{
|
||||||
|
return SetPixelResult.NotEnoughMoney;
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
var updates = await uow.GetTable<NCPixel>()
|
||||||
|
.Where(x => x.Position == position && x.Price <= price)
|
||||||
|
.UpdateAsync(old => new NCPixel()
|
||||||
|
{
|
||||||
|
Position = position,
|
||||||
|
Color = color,
|
||||||
|
Text = text,
|
||||||
|
OwnerId = userId,
|
||||||
|
Price = price + 1
|
||||||
|
});
|
||||||
|
success = updates > 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
await wallet.Add(price, new("canvas", "pixel-refund", $"Refund pixel {new kwum(position)} purchase"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return success ? SetPixelResult.Success : SetPixelResult.InsufficientPayment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SetImage(uint[] colors)
|
||||||
|
{
|
||||||
|
if (colors.Length != CANVAS_WIDTH * CANVAS_HEIGHT)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
await uow.GetTable<NCPixel>().DeleteAsync();
|
||||||
|
await uow.GetTable<NCPixel>()
|
||||||
|
.BulkCopyAsync(colors.Select((x, i) => new NCPixel()
|
||||||
|
{
|
||||||
|
Color = x,
|
||||||
|
Price = INITIAL_PRICE,
|
||||||
|
Position = i,
|
||||||
|
Text = "",
|
||||||
|
OwnerId = 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<NCPixel?> GetPixel(int x, int y)
|
||||||
|
{
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(x);
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(y);
|
||||||
|
|
||||||
|
if (x >= CANVAS_WIDTH || y >= CANVAS_HEIGHT)
|
||||||
|
return Task.FromResult<NCPixel?>(null);
|
||||||
|
|
||||||
|
return GetPixel(x + (y * CANVAS_WIDTH));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<NCPixel?> GetPixel(int position)
|
||||||
|
{
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(position);
|
||||||
|
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
return await uow.GetTable<NCPixel>().FirstOrDefaultAsync(x => x.Position == position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<NCPixel[]> GetPixelGroup(int position)
|
||||||
|
{
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(position);
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(position, CANVAS_WIDTH * CANVAS_HEIGHT);
|
||||||
|
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
return await uow.GetTable<NCPixel>()
|
||||||
|
.Where(x => x.Position % CANVAS_WIDTH >= (position % CANVAS_WIDTH) - 2
|
||||||
|
&& x.Position % CANVAS_WIDTH <= (position % CANVAS_WIDTH) + 2
|
||||||
|
&& x.Position / CANVAS_WIDTH >= (position / CANVAS_WIDTH) - 2
|
||||||
|
&& x.Position / CANVAS_WIDTH <= (position / CANVAS_WIDTH) + 2)
|
||||||
|
.OrderBy(x => x.Position)
|
||||||
|
.ToArrayAsyncLinqToDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHeight()
|
||||||
|
=> CANVAS_HEIGHT;
|
||||||
|
|
||||||
|
public int GetWidth()
|
||||||
|
=> CANVAS_WIDTH;
|
||||||
|
}
|
9
src/NadekoBot/Modules/Games/NCanvas/SetPixelResult.cs
Normal file
9
src/NadekoBot/Modules/Games/NCanvas/SetPixelResult.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace NadekoBot.Modules.Games;
|
||||||
|
|
||||||
|
public enum SetPixelResult
|
||||||
|
{
|
||||||
|
Success,
|
||||||
|
InsufficientPayment,
|
||||||
|
NotEnoughMoney,
|
||||||
|
InvalidInput
|
||||||
|
}
|
@@ -12,9 +12,9 @@ public sealed partial class Music
|
|||||||
{
|
{
|
||||||
private static readonly SemaphoreSlim _playlistLock = new(1, 1);
|
private static readonly SemaphoreSlim _playlistLock = new(1, 1);
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
|
|
||||||
public PlaylistCommands(DbService db, IBotCredentials creds)
|
public PlaylistCommands(DbService db, IBotCreds creds)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_creds = creds;
|
_creds = creds;
|
||||||
|
@@ -43,8 +43,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
|
|||||||
+ "--no-check-certificate "
|
+ "--no-check-certificate "
|
||||||
+ "-i "
|
+ "-i "
|
||||||
+ "--yes-playlist "
|
+ "--yes-playlist "
|
||||||
+ "-- \"{0}\"",
|
+ "-- \"{0}\"");
|
||||||
scs.Data.YtProvider != YoutubeSearcher.Ytdl);
|
|
||||||
|
|
||||||
_ytdlIdOperation = new("-4 "
|
_ytdlIdOperation = new("-4 "
|
||||||
+ "--geo-bypass "
|
+ "--geo-bypass "
|
||||||
@@ -56,8 +55,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
|
|||||||
+ "--get-thumbnail "
|
+ "--get-thumbnail "
|
||||||
+ "--get-duration "
|
+ "--get-duration "
|
||||||
+ "--no-check-certificate "
|
+ "--no-check-certificate "
|
||||||
+ "-- \"{0}\"",
|
+ "-- \"{0}\"");
|
||||||
scs.Data.YtProvider != YoutubeSearcher.Ytdl);
|
|
||||||
|
|
||||||
_ytdlSearchOperation = new("-4 "
|
_ytdlSearchOperation = new("-4 "
|
||||||
+ "--geo-bypass "
|
+ "--geo-bypass "
|
||||||
@@ -70,8 +68,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
|
|||||||
+ "--get-duration "
|
+ "--get-duration "
|
||||||
+ "--no-check-certificate "
|
+ "--no-check-certificate "
|
||||||
+ "--default-search "
|
+ "--default-search "
|
||||||
+ "\"ytsearch:\" -- \"{0}\"",
|
+ "\"ytsearch:\" -- \"{0}\"");
|
||||||
scs.Data.YtProvider != YoutubeSearcher.Ytdl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private YtTrackData ResolveYtdlData(string ytdlOutputString)
|
private YtTrackData ResolveYtdlData(string ytdlOutputString)
|
||||||
@@ -291,6 +288,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
|
|||||||
|
|
||||||
public Task<string?> GetStreamUrl(string videoId)
|
public Task<string?> GetStreamUrl(string videoId)
|
||||||
=> CreateCacherFactory(videoId)();
|
=> CreateCacherFactory(videoId)();
|
||||||
|
|
||||||
private readonly struct YtTrackData
|
private readonly struct YtTrackData
|
||||||
{
|
{
|
||||||
public readonly string Title;
|
public readonly string Title;
|
||||||
|
@@ -16,11 +16,11 @@ public class CryptoService : INService
|
|||||||
{
|
{
|
||||||
private readonly IBotCache _cache;
|
private readonly IBotCache _cache;
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
|
|
||||||
private readonly SemaphoreSlim _getCryptoLock = new(1, 1);
|
private readonly SemaphoreSlim _getCryptoLock = new(1, 1);
|
||||||
|
|
||||||
public CryptoService(IBotCache cache, IHttpClientFactory httpFactory, IBotCredentials creds)
|
public CryptoService(IBotCache cache, IHttpClientFactory httpFactory, IBotCreds creds)
|
||||||
{
|
{
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_httpFactory = httpFactory;
|
_httpFactory = httpFactory;
|
||||||
|
@@ -9,10 +9,10 @@ public partial class Searches
|
|||||||
[Group]
|
[Group]
|
||||||
public partial class OsuCommands : NadekoModule<OsuService>
|
public partial class OsuCommands : NadekoModule<OsuService>
|
||||||
{
|
{
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
|
|
||||||
public OsuCommands(IBotCredentials creds, IHttpClientFactory factory)
|
public OsuCommands(IBotCreds creds, IHttpClientFactory factory)
|
||||||
{
|
{
|
||||||
_creds = creds;
|
_creds = creds;
|
||||||
_httpFactory = factory;
|
_httpFactory = factory;
|
||||||
|
@@ -7,9 +7,9 @@ namespace NadekoBot.Modules.Searches;
|
|||||||
public sealed class OsuService : INService
|
public sealed class OsuService : INService
|
||||||
{
|
{
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
|
|
||||||
public OsuService(IHttpClientFactory httpFactory, IBotCredentials creds)
|
public OsuService(IHttpClientFactory httpFactory, IBotCreds creds)
|
||||||
{
|
{
|
||||||
_httpFactory = httpFactory;
|
_httpFactory = httpFactory;
|
||||||
_creds = creds;
|
_creds = creds;
|
||||||
|
@@ -7,10 +7,9 @@ public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, INServi
|
|||||||
{
|
{
|
||||||
private readonly SearchesConfigService _scs;
|
private readonly SearchesConfigService _scs;
|
||||||
private readonly SearxSearchService _sss;
|
private readonly SearxSearchService _sss;
|
||||||
|
private readonly YtDlpSearchService _ytdlp;
|
||||||
private readonly GoogleSearchService _gss;
|
private readonly GoogleSearchService _gss;
|
||||||
|
|
||||||
private readonly YtdlpYoutubeSearchService _ytdlp;
|
|
||||||
private readonly YtdlYoutubeSearchService _ytdl;
|
|
||||||
private readonly YoutubeDataApiSearchService _ytdata;
|
private readonly YoutubeDataApiSearchService _ytdata;
|
||||||
private readonly InvidiousYtSearchService _iYtSs;
|
private readonly InvidiousYtSearchService _iYtSs;
|
||||||
private readonly GoogleScrapeService _gscs;
|
private readonly GoogleScrapeService _gscs;
|
||||||
@@ -20,19 +19,17 @@ public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, INServi
|
|||||||
GoogleSearchService gss,
|
GoogleSearchService gss,
|
||||||
GoogleScrapeService gscs,
|
GoogleScrapeService gscs,
|
||||||
SearxSearchService sss,
|
SearxSearchService sss,
|
||||||
YtdlpYoutubeSearchService ytdlp,
|
YtDlpSearchService ytdlp,
|
||||||
YtdlYoutubeSearchService ytdl,
|
|
||||||
YoutubeDataApiSearchService ytdata,
|
YoutubeDataApiSearchService ytdata,
|
||||||
InvidiousYtSearchService iYtSs)
|
InvidiousYtSearchService iYtSs)
|
||||||
{
|
{
|
||||||
_scs = scs;
|
_scs = scs;
|
||||||
_sss = sss;
|
_sss = sss;
|
||||||
|
_ytdlp = ytdlp;
|
||||||
_gss = gss;
|
_gss = gss;
|
||||||
_gscs = gscs;
|
_gscs = gscs;
|
||||||
_iYtSs = iYtSs;
|
_iYtSs = iYtSs;
|
||||||
|
|
||||||
_ytdlp = ytdlp;
|
|
||||||
_ytdl = ytdl;
|
|
||||||
_ytdata = ytdata;
|
_ytdata = ytdata;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,9 +54,8 @@ public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, INServi
|
|||||||
=> _scs.Data.YtProvider switch
|
=> _scs.Data.YtProvider switch
|
||||||
{
|
{
|
||||||
YoutubeSearcher.YtDataApiv3 => _ytdata,
|
YoutubeSearcher.YtDataApiv3 => _ytdata,
|
||||||
YoutubeSearcher.Ytdlp => _ytdlp,
|
|
||||||
YoutubeSearcher.Ytdl => _ytdl,
|
|
||||||
YoutubeSearcher.Invidious => _iYtSs,
|
YoutubeSearcher.Invidious => _iYtSs,
|
||||||
_ => _ytdl
|
YoutubeSearcher.Ytdlp => _ytdlp,
|
||||||
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
};
|
};
|
||||||
}
|
}
|
@@ -93,16 +93,12 @@ public partial class Searches
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var embeds = new List<EmbedBuilder>(4);
|
|
||||||
|
|
||||||
|
|
||||||
EmbedBuilder CreateEmbed(IImageSearchResultEntry entry)
|
EmbedBuilder CreateEmbed(IImageSearchResultEntry entry)
|
||||||
{
|
{
|
||||||
return _sender.CreateEmbed()
|
return _sender.CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithAuthor(ctx.User)
|
.WithAuthor(ctx.User)
|
||||||
.WithTitle(query)
|
.WithTitle(query)
|
||||||
.WithUrl("https://google.com")
|
|
||||||
.WithImageUrl(entry.Link);
|
.WithImageUrl(entry.Link);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,55 +116,50 @@ public partial class Searches
|
|||||||
.WithDescription(GetText(strs.no_search_results));
|
.WithDescription(GetText(strs.no_search_results));
|
||||||
|
|
||||||
var embed = CreateEmbed(item);
|
var embed = CreateEmbed(item);
|
||||||
embeds.Add(embed);
|
|
||||||
|
|
||||||
return embed;
|
return embed;
|
||||||
})
|
})
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private TypedKey<string> GetYtCacheKey(string query)
|
private TypedKey<string[]> GetYtCacheKey(string query)
|
||||||
=> new($"search:youtube:{query}");
|
=> new($"search:yt:{query}");
|
||||||
|
|
||||||
private async Task AddYoutubeUrlToCacheAsync(string query, string url)
|
private async Task AddYoutubeUrlToCacheAsync(string query, string[] url)
|
||||||
=> await _cache.AddAsync(GetYtCacheKey(query), url, expiry: 1.Hours());
|
=> await _cache.AddAsync(GetYtCacheKey(query), url, expiry: 1.Hours());
|
||||||
|
|
||||||
private async Task<VideoInfo?> GetYoutubeUrlFromCacheAsync(string query)
|
private async Task<VideoInfo[]?> GetYoutubeUrlFromCacheAsync(string query)
|
||||||
{
|
{
|
||||||
var result = await _cache.GetAsync(GetYtCacheKey(query));
|
var result = await _cache.GetAsync(GetYtCacheKey(query));
|
||||||
|
|
||||||
if (!result.TryGetValue(out var url) || string.IsNullOrWhiteSpace(url))
|
if (!result.TryGetValue(out var urls) || urls.Length == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new VideoInfo()
|
return urls.Map(url => new VideoInfo()
|
||||||
{
|
{
|
||||||
Url = url
|
Url = url
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
public async Task Youtube([Leftover] string? query = null)
|
public async Task Youtube([Leftover] string query)
|
||||||
{
|
{
|
||||||
query = query?.Trim();
|
query = query.Trim();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(query))
|
|
||||||
{
|
|
||||||
await Response().Error(strs.specify_search_params).SendAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = ctx.Channel.TriggerTypingAsync();
|
_ = ctx.Channel.TriggerTypingAsync();
|
||||||
|
|
||||||
var maybeResult = await GetYoutubeUrlFromCacheAsync(query)
|
var maybeResults = await GetYoutubeUrlFromCacheAsync(query)
|
||||||
?? await _searchFactory.GetYoutubeSearchService().SearchAsync(query);
|
?? await _searchFactory.GetYoutubeSearchService().SearchAsync(query);
|
||||||
if (maybeResult is not { } result || result is { Url: null })
|
|
||||||
|
if (maybeResults is not { } result || result.Length == 0)
|
||||||
{
|
{
|
||||||
await Response().Error(strs.no_results).SendAsync();
|
await Response().Error(strs.no_results).SendAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await AddYoutubeUrlToCacheAsync(query, result.Url);
|
await AddYoutubeUrlToCacheAsync(query, result.Map(x => x.Url));
|
||||||
await Response().Text(result.Url).SendAsync();
|
|
||||||
|
await Response().Text(result[0].Url).SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Cmd]
|
// [Cmd]
|
||||||
|
@@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
public interface IYoutubeSearchService
|
public interface IYoutubeSearchService
|
||||||
{
|
{
|
||||||
Task<VideoInfo?> SearchAsync(string query);
|
Task<VideoInfo[]?> SearchAsync(string query);
|
||||||
}
|
}
|
@@ -18,7 +18,7 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, INService
|
|||||||
_rng = new();
|
_rng = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<VideoInfo?> SearchAsync(string query)
|
public async Task<VideoInfo[]?> SearchAsync(string query)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(query);
|
ArgumentNullException.ThrowIfNull(query);
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, INService
|
|||||||
var url = $"{instance}/api/v1/search"
|
var url = $"{instance}/api/v1/search"
|
||||||
+ $"?q={query}"
|
+ $"?q={query}"
|
||||||
+ $"&type=video";
|
+ $"&type=video";
|
||||||
|
|
||||||
using var http = _http.CreateClient();
|
using var http = _http.CreateClient();
|
||||||
var res = await http.GetFromJsonAsync<List<InvidiousSearchResponse>>(
|
var res = await http.GetFromJsonAsync<List<InvidiousSearchResponse>>(
|
||||||
url);
|
url);
|
||||||
@@ -42,6 +43,6 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, INService
|
|||||||
if (res is null or { Count: 0 })
|
if (res is null or { Count: 0 })
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new VideoInfo(res[0].VideoId);
|
return res.Map(r => new VideoInfo(r.VideoId));
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -9,18 +9,18 @@ public sealed class YoutubeDataApiSearchService : IYoutubeSearchService, INServi
|
|||||||
_gapi = gapi;
|
_gapi = gapi;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<VideoInfo?> SearchAsync(string query)
|
public async Task<VideoInfo[]?> SearchAsync(string query)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(query);
|
ArgumentNullException.ThrowIfNull(query);
|
||||||
|
|
||||||
var results = await _gapi.GetVideoLinksByKeywordAsync(query);
|
var results = await _gapi.GetVideoLinksByKeywordAsync(query);
|
||||||
var first = results.FirstOrDefault();
|
|
||||||
if (first is null)
|
if(results.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new()
|
return results.Map(r => new VideoInfo()
|
||||||
{
|
{
|
||||||
Url = first
|
Url = r
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,26 @@
|
|||||||
|
namespace NadekoBot.Modules.Searches.Youtube;
|
||||||
|
|
||||||
|
public class YtDlpSearchService : IYoutubeSearchService, INService
|
||||||
|
{
|
||||||
|
private YtdlOperation CreateYtdlOp(int count)
|
||||||
|
=> new YtdlOperation("-4 "
|
||||||
|
+ "--ignore-errors --flat-playlist --skip-download --quiet "
|
||||||
|
+ "--geo-bypass "
|
||||||
|
+ "--encoding UTF8 "
|
||||||
|
+ "--get-id "
|
||||||
|
+ "--no-check-certificate "
|
||||||
|
+ "--default-search "
|
||||||
|
+ $"\"ytsearch{count}:\" -- \"{{0}}\"");
|
||||||
|
|
||||||
|
public async Task<VideoInfo[]?> SearchAsync(string query)
|
||||||
|
{
|
||||||
|
var op = CreateYtdlOp(5);
|
||||||
|
var data = await op.GetDataAsync(query);
|
||||||
|
var items = data?.Split('\n');
|
||||||
|
if (items is null or { Length: 0 })
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return items
|
||||||
|
.Map(x => new VideoInfo(x));
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +0,0 @@
|
|||||||
namespace NadekoBot.Modules.Searches.Youtube;
|
|
||||||
|
|
||||||
public sealed class YtdlYoutubeSearchService : YoutubedlxServiceBase, INService
|
|
||||||
{
|
|
||||||
public override async Task<VideoInfo?> SearchAsync(string query)
|
|
||||||
=> await InternalGetInfoAsync(query, false);
|
|
||||||
}
|
|
@@ -1,7 +0,0 @@
|
|||||||
namespace NadekoBot.Modules.Searches.Youtube;
|
|
||||||
|
|
||||||
public sealed class YtdlpYoutubeSearchService : YoutubedlxServiceBase, INService
|
|
||||||
{
|
|
||||||
public override async Task<VideoInfo?> SearchAsync(string query)
|
|
||||||
=> await InternalGetInfoAsync(query, true);
|
|
||||||
}
|
|
@@ -1,34 +0,0 @@
|
|||||||
namespace NadekoBot.Modules.Searches.Youtube;
|
|
||||||
|
|
||||||
public abstract class YoutubedlxServiceBase : IYoutubeSearchService
|
|
||||||
{
|
|
||||||
private YtdlOperation CreateYtdlOp(bool isYtDlp)
|
|
||||||
=> new YtdlOperation("-4 "
|
|
||||||
+ "--geo-bypass "
|
|
||||||
+ "--encoding UTF8 "
|
|
||||||
+ "--get-id "
|
|
||||||
+ "--no-check-certificate "
|
|
||||||
+ "--default-search "
|
|
||||||
+ "\"ytsearch:\" -- \"{0}\"",
|
|
||||||
isYtDlp: isYtDlp);
|
|
||||||
|
|
||||||
protected async Task<VideoInfo?> InternalGetInfoAsync(string query, bool isYtDlp)
|
|
||||||
{
|
|
||||||
var op = CreateYtdlOp(isYtDlp);
|
|
||||||
var data = await op.GetDataAsync(query);
|
|
||||||
var items = data?.Split('\n');
|
|
||||||
if (items is null or { Length: 0 })
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var id = items.FirstOrDefault(x => x.Length is > 5 and < 15);
|
|
||||||
if (id is null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new VideoInfo()
|
|
||||||
{
|
|
||||||
Url = $"https://youtube.com/watch?v={id}"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract Task<VideoInfo?> SearchAsync(string query);
|
|
||||||
}
|
|
@@ -13,14 +13,14 @@ namespace NadekoBot.Modules.Searches;
|
|||||||
|
|
||||||
public partial class Searches : NadekoModule<SearchesService>
|
public partial class Searches : NadekoModule<SearchesService>
|
||||||
{
|
{
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
private readonly IGoogleApiService _google;
|
private readonly IGoogleApiService _google;
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
private readonly ITimezoneService _tzSvc;
|
private readonly ITimezoneService _tzSvc;
|
||||||
|
|
||||||
public Searches(
|
public Searches(
|
||||||
IBotCredentials creds,
|
IBotCreds creds,
|
||||||
IGoogleApiService google,
|
IGoogleApiService google,
|
||||||
IHttpClientFactory factory,
|
IHttpClientFactory factory,
|
||||||
IMemoryCache cache,
|
IMemoryCache cache,
|
||||||
|
@@ -28,11 +28,9 @@ public partial class SearchesConfig : ICloneable<SearchesConfig>
|
|||||||
[Comment("""
|
[Comment("""
|
||||||
Which search provider will be used for the `.youtube` and `.q` commands.
|
Which search provider will be used for the `.youtube` and `.q` commands.
|
||||||
|
|
||||||
- `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console
|
- `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console. `.q` is not supported for this setting. It will fallback to yt-dlp.
|
||||||
|
|
||||||
- `ytdl` - default, uses youtube-dl. Requires `youtube-dl` to be installed and it's path added to env variables. Slow.
|
- `ytdlp` - default, recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables
|
||||||
|
|
||||||
- `ytdlp` - recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables
|
|
||||||
|
|
||||||
- `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property
|
- `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property
|
||||||
""")]
|
""")]
|
||||||
@@ -77,9 +75,9 @@ public sealed class FollowedStreamConfig
|
|||||||
|
|
||||||
public enum YoutubeSearcher
|
public enum YoutubeSearcher
|
||||||
{
|
{
|
||||||
YtDataApiv3,
|
YtDataApiv3 = 0,
|
||||||
Ytdl,
|
Ytdl = 1,
|
||||||
Ytdlp,
|
Ytdlp = 1,
|
||||||
Invid,
|
Invid = 3,
|
||||||
Invidious = 3
|
Invidious = 3
|
||||||
}
|
}
|
@@ -47,19 +47,11 @@ public class SearchesConfigService : ConfigServiceBase<SearchesConfig>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.Version < 2)
|
if (data.Version < 4)
|
||||||
{
|
{
|
||||||
ModifyConfig(c =>
|
ModifyConfig(c =>
|
||||||
{
|
{
|
||||||
c.Version = 2;
|
c.Version = 4;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.Version < 3)
|
|
||||||
{
|
|
||||||
ModifyConfig(c =>
|
|
||||||
{
|
|
||||||
c.Version = 3;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -100,10 +100,6 @@ public sealed class AiAssistantService
|
|||||||
|
|
||||||
using var client = _httpFactory.CreateClient();
|
using var client = _httpFactory.CreateClient();
|
||||||
|
|
||||||
// todo customize according to the bot's config
|
|
||||||
// - CurrencyName
|
|
||||||
// -
|
|
||||||
|
|
||||||
using var response = await client.SendAsync(request);
|
using var response = await client.SendAsync(request);
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||||
|
@@ -11,7 +11,7 @@ public sealed class GiveawayService : INService, IReadyExecutor
|
|||||||
public static string GiveawayEmoji = "🎉";
|
public static string GiveawayEmoji = "🎉";
|
||||||
|
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly IMessageSenderService _sender;
|
private readonly IMessageSenderService _sender;
|
||||||
private readonly IBotStrings _strings;
|
private readonly IBotStrings _strings;
|
||||||
@@ -20,7 +20,7 @@ public sealed class GiveawayService : INService, IReadyExecutor
|
|||||||
private SortedSet<GiveawayModel> _giveawayCache = new SortedSet<GiveawayModel>();
|
private SortedSet<GiveawayModel> _giveawayCache = new SortedSet<GiveawayModel>();
|
||||||
private readonly NadekoRandom _rng;
|
private readonly NadekoRandom _rng;
|
||||||
|
|
||||||
public GiveawayService(DbService db, IBotCredentials creds, DiscordSocketClient client,
|
public GiveawayService(DbService db, IBotCreds creds, DiscordSocketClient client,
|
||||||
IMessageSenderService sender, IBotStrings strings, ILocalization localization, IMemoryCache cache)
|
IMessageSenderService sender, IBotStrings strings, ILocalization localization, IMemoryCache cache)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
|
@@ -26,6 +26,8 @@ public interface IQuoteService
|
|||||||
ulong guildId,
|
ulong guildId,
|
||||||
string? keyword,
|
string? keyword,
|
||||||
string text);
|
string text);
|
||||||
|
|
||||||
|
Task<(IReadOnlyCollection<Quote> quotes, int totalCount)> FindQuotesAsync(ulong guildId, string query, int page);
|
||||||
|
|
||||||
Task<IReadOnlyCollection<Quote>> GetGuildQuotesAsync(ulong guildId);
|
Task<IReadOnlyCollection<Quote>> GetGuildQuotesAsync(ulong guildId);
|
||||||
Task<int> RemoveAllByKeyword(ulong guildId, string keyword);
|
Task<int> RemoveAllByKeyword(ulong guildId, string keyword);
|
||||||
@@ -39,6 +41,7 @@ public interface IQuoteService
|
|||||||
string text);
|
string text);
|
||||||
|
|
||||||
Task<Quote?> EditQuoteAsync(ulong authorId, int quoteId, string text);
|
Task<Quote?> EditQuoteAsync(ulong authorId, int quoteId, string text);
|
||||||
|
Task<Quote?> EditQuoteAsync(ulong guildId, int quoteId, string keyword, string text);
|
||||||
|
|
||||||
Task<bool> DeleteQuoteAsync(
|
Task<bool> DeleteQuoteAsync(
|
||||||
ulong guildId,
|
ulong guildId,
|
||||||
|
@@ -169,6 +169,23 @@ public sealed class QuoteService : IQuoteService, INService
|
|||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Quote?> EditQuoteAsync(
|
||||||
|
ulong guildId,
|
||||||
|
int quoteId,
|
||||||
|
string keyword,
|
||||||
|
string text)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
var result = await uow.GetTable<Quote>()
|
||||||
|
.Where(x => x.Id == quoteId && x.GuildId == guildId)
|
||||||
|
.Set(x => x.Keyword, keyword)
|
||||||
|
.Set(x => x.Text, text)
|
||||||
|
.UpdateWithOutputAsync((del, ins) => ins);
|
||||||
|
|
||||||
|
var q = result.FirstOrDefault();
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> DeleteQuoteAsync(
|
public async Task<bool> DeleteQuoteAsync(
|
||||||
ulong guildId,
|
ulong guildId,
|
||||||
ulong authorId,
|
ulong authorId,
|
||||||
@@ -219,4 +236,24 @@ public sealed class QuoteService : IQuoteService, INService
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<(IReadOnlyCollection<Quote> quotes, int totalCount)> FindQuotesAsync(
|
||||||
|
ulong guildId,
|
||||||
|
string query,
|
||||||
|
int page)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
|
var baseQuery = uow.GetTable<Quote>()
|
||||||
|
.Where(x => x.GuildId == guildId)
|
||||||
|
.Where(x => x.Keyword.Contains(query) || x.Text.Contains(query));
|
||||||
|
|
||||||
|
var quotes = await baseQuery
|
||||||
|
.OrderBy(x => x.Id)
|
||||||
|
.Skip((page - 1) * 10)
|
||||||
|
.Take(10)
|
||||||
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
|
return (quotes, await baseQuery.CountAsyncLinqToDB());
|
||||||
|
}
|
||||||
}
|
}
|
@@ -17,14 +17,14 @@ public class RemindService : INService, IReadyExecutor, IRemindService
|
|||||||
|
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
private readonly IMessageSenderService _sender;
|
private readonly IMessageSenderService _sender;
|
||||||
private readonly CultureInfo _culture;
|
private readonly CultureInfo _culture;
|
||||||
|
|
||||||
public RemindService(
|
public RemindService(
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
DbService db,
|
DbService db,
|
||||||
IBotCredentials creds,
|
IBotCreds creds,
|
||||||
IMessageSenderService sender)
|
IMessageSenderService sender)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
@@ -197,26 +197,29 @@ public class RemindService : INService, IReadyExecutor, IRemindService
|
|||||||
|
|
||||||
var st = SmartText.CreateFrom(r.Message);
|
var st = SmartText.CreateFrom(r.Message);
|
||||||
|
|
||||||
|
var res = _sender.Response(ch)
|
||||||
|
.UserBasedMentions(_client.GetGuild(r.ServerId)?.GetUser(r.UserId));
|
||||||
|
|
||||||
if (st is SmartEmbedText set)
|
if (st is SmartEmbedText set)
|
||||||
{
|
{
|
||||||
await _sender.Response(ch).Embed(set.GetEmbed()).SendAsync();
|
await res.Embed(set.GetEmbed()).SendAsync();
|
||||||
}
|
}
|
||||||
else if (st is SmartEmbedTextArray seta)
|
else if (st is SmartEmbedTextArray seta)
|
||||||
{
|
{
|
||||||
await _sender.Response(ch).Embeds(seta.GetEmbedBuilders()).SendAsync();
|
await res.Embeds(seta.GetEmbedBuilders()).SendAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await _sender.Response(ch)
|
await res
|
||||||
.Embed(_sender.CreateEmbed()
|
.Embed(_sender.CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithTitle("Reminder")
|
.WithTitle("Reminder")
|
||||||
.AddField("Created At",
|
.AddField("Created At",
|
||||||
r.DateAdded.HasValue ? r.DateAdded.Value.ToLongDateString() : "?")
|
r.DateAdded.HasValue ? r.DateAdded.Value.ToLongDateString() : "?")
|
||||||
.AddField("By",
|
.AddField("By",
|
||||||
(await ch.GetUserAsync(r.UserId))?.ToString() ?? r.UserId.ToString()))
|
(await ch.GetUserAsync(r.UserId))?.ToString() ?? r.UserId.ToString()))
|
||||||
.Text(r.Message)
|
.Text(r.Message)
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@@ -12,7 +12,7 @@ public sealed class RepeaterService : IReadyExecutor, INService
|
|||||||
|
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
private readonly IReplacementService _repSvc;
|
private readonly IReplacementService _repSvc;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly LinkedList<RunningRepeater> _repeaterQueue;
|
private readonly LinkedList<RunningRepeater> _repeaterQueue;
|
||||||
private readonly ConcurrentHashSet<int> _noRedundant;
|
private readonly ConcurrentHashSet<int> _noRedundant;
|
||||||
@@ -25,7 +25,7 @@ public sealed class RepeaterService : IReadyExecutor, INService
|
|||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
DbService db,
|
DbService db,
|
||||||
IReplacementService repSvc,
|
IReplacementService repSvc,
|
||||||
IBotCredentials creds,
|
IBotCreds creds,
|
||||||
IMessageSenderService sender)
|
IMessageSenderService sender)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
|
@@ -34,7 +34,7 @@ public partial class Utility : NadekoModule
|
|||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly ICoordinator _coord;
|
private readonly ICoordinator _coord;
|
||||||
private readonly IStatsService _stats;
|
private readonly IStatsService _stats;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
private readonly DownloadTracker _tracker;
|
private readonly DownloadTracker _tracker;
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
private readonly VerboseErrorsService _veService;
|
private readonly VerboseErrorsService _veService;
|
||||||
@@ -45,7 +45,7 @@ public partial class Utility : NadekoModule
|
|||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
ICoordinator coord,
|
ICoordinator coord,
|
||||||
IStatsService stats,
|
IStatsService stats,
|
||||||
IBotCredentials creds,
|
IBotCreds creds,
|
||||||
DownloadTracker tracker,
|
DownloadTracker tracker,
|
||||||
IHttpClientFactory httpFactory,
|
IHttpClientFactory httpFactory,
|
||||||
VerboseErrorsService veService,
|
VerboseErrorsService veService,
|
||||||
|
@@ -107,7 +107,7 @@ public partial class Xp : NadekoModule<XpService>
|
|||||||
[Cmd]
|
[Cmd]
|
||||||
[UserPerm(GuildPerm.ManageChannels)]
|
[UserPerm(GuildPerm.ManageChannels)]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
public async Task XpExclude(Channel _, [Leftover] IChannel channel = null)
|
public async Task XpExclude(Channel _, [Leftover] IChannel? channel = null)
|
||||||
{
|
{
|
||||||
if (channel is null)
|
if (channel is null)
|
||||||
channel = ctx.Channel;
|
channel = ctx.Channel;
|
||||||
@@ -182,29 +182,28 @@ public partial class Xp : NadekoModule<XpService>
|
|||||||
var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
|
var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
|
||||||
|
|
||||||
await ctx.Channel.TriggerTypingAsync();
|
await ctx.Channel.TriggerTypingAsync();
|
||||||
|
|
||||||
var socketGuild = (SocketGuild)ctx.Guild;
|
|
||||||
var allCleanUsers = new List<UserXpStats>();
|
|
||||||
if (opts.Clean)
|
if (opts.Clean)
|
||||||
{
|
{
|
||||||
await ctx.Channel.TriggerTypingAsync();
|
|
||||||
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
|
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
|
||||||
|
|
||||||
allCleanUsers = (await _service.GetTopUserXps(ctx.Guild.Id, 1000))
|
|
||||||
.Where(user => socketGuild.GetUser(user.UserId) is not null)
|
|
||||||
.ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var res = opts.Clean
|
async Task<IReadOnlyCollection<UserXpStats>> GetPageItems(int curPage)
|
||||||
? Response()
|
{
|
||||||
.Paginated()
|
var socketGuild = (SocketGuild)ctx.Guild;
|
||||||
.Items(allCleanUsers)
|
if (opts.Clean)
|
||||||
: Response()
|
{
|
||||||
.Paginated()
|
return await _service.GetGuildUserXps(ctx.Guild.Id,
|
||||||
.PageItems((curPage) => _service.GetUserXps(ctx.Guild.Id, curPage));
|
socketGuild.Users.Select(x => x.Id).ToList(),
|
||||||
|
curPage);
|
||||||
|
}
|
||||||
|
|
||||||
await res
|
return await _service.GetGuildUserXps(ctx.Guild.Id, curPage);
|
||||||
.PageSize(9)
|
}
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Paginated()
|
||||||
|
.PageItems(GetPageItems)
|
||||||
|
.PageSize(10)
|
||||||
.CurrentPage(page)
|
.CurrentPage(page)
|
||||||
.Page((users, curPage) =>
|
.Page((users, curPage) =>
|
||||||
{
|
{
|
||||||
@@ -237,15 +236,33 @@ public partial class Xp : NadekoModule<XpService>
|
|||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
public async Task XpGlobalLeaderboard(int page = 1)
|
public async Task XpGlobalLeaderboard(int page = 1, params string[] args)
|
||||||
{
|
{
|
||||||
if (--page < 0 || page > 99)
|
if (--page < 0 || page > 99)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
|
||||||
|
|
||||||
|
await ctx.Channel.TriggerTypingAsync();
|
||||||
|
if (opts.Clean)
|
||||||
|
{
|
||||||
|
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<IReadOnlyCollection<DiscordUser>> GetPageItems(int curPage)
|
||||||
|
{
|
||||||
|
if (opts.Clean)
|
||||||
|
{
|
||||||
|
return await _service.GetGlobalUserXps(page, ((SocketGuild)ctx.Guild).Users.Select(x => x.Id).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _service.GetGlobalUserXps(curPage);
|
||||||
|
}
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Paginated()
|
.Paginated()
|
||||||
.PageItems(async curPage => await _service.GetUserXps(curPage))
|
.PageItems(GetPageItems)
|
||||||
.PageSize(9)
|
.PageSize(10)
|
||||||
.Page((users, curPage) =>
|
.Page((users, curPage) =>
|
||||||
{
|
{
|
||||||
var embed = _sender.CreateEmbed()
|
var embed = _sender.CreateEmbed()
|
||||||
@@ -282,7 +299,9 @@ public partial class Xp : NadekoModule<XpService>
|
|||||||
if (role.IsManaged)
|
if (role.IsManaged)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var count = await _service.AddXpToUsersAsync(ctx.Guild.Id, amount, role.Members.Select(x => x.Id).ToArray());
|
var count = await _service.AddXpToUsersAsync(ctx.Guild.Id,
|
||||||
|
amount,
|
||||||
|
role.Members.Select(x => x.Id).ToArray());
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm(
|
.Confirm(
|
||||||
strs.xpadd_users(Format.Bold(amount.ToString()), Format.Bold(count.ToString())))
|
strs.xpadd_users(Format.Bold(amount.ToString()), Format.Bold(count.ToString())))
|
||||||
|
@@ -12,6 +12,7 @@ using SixLabors.ImageSharp.PixelFormats;
|
|||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using LinqToDB.EntityFrameworkCore;
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using LinqToDB.Tools;
|
||||||
using NadekoBot.Modules.Patronage;
|
using NadekoBot.Modules.Patronage;
|
||||||
using Color = SixLabors.ImageSharp.Color;
|
using Color = SixLabors.ImageSharp.Color;
|
||||||
using Exception = System.Exception;
|
using Exception = System.Exception;
|
||||||
@@ -25,7 +26,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
private readonly IImageCache _images;
|
private readonly IImageCache _images;
|
||||||
private readonly IBotStrings _strings;
|
private readonly IBotStrings _strings;
|
||||||
private readonly FontProvider _fonts;
|
private readonly FontProvider _fonts;
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCreds _creds;
|
||||||
private readonly ICurrencyService _cs;
|
private readonly ICurrencyService _cs;
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
private readonly XpConfigService _xpConfig;
|
private readonly XpConfigService _xpConfig;
|
||||||
@@ -55,7 +56,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
IImageCache images,
|
IImageCache images,
|
||||||
IBotCache c,
|
IBotCache c,
|
||||||
FontProvider fonts,
|
FontProvider fonts,
|
||||||
IBotCredentials creds,
|
IBotCreds creds,
|
||||||
ICurrencyService cs,
|
ICurrencyService cs,
|
||||||
IHttpClientFactory http,
|
IHttpClientFactory http,
|
||||||
XpConfigService xpConfig,
|
XpConfigService xpConfig,
|
||||||
@@ -563,23 +564,50 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
uow.SaveChanges();
|
uow.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyCollection<UserXpStats>> GetUserXps(ulong guildId, int page)
|
public async Task<IReadOnlyCollection<UserXpStats>> GetGuildUserXps(ulong guildId, int page)
|
||||||
{
|
{
|
||||||
await using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
return await uow.Set<UserXpStats>().GetUsersFor(guildId, page);
|
return await uow
|
||||||
|
.UserXpStats
|
||||||
|
.Where(x => x.GuildId == guildId)
|
||||||
|
.OrderByDescending(x => x.Xp + x.AwardedXp)
|
||||||
|
.Skip(page * 10)
|
||||||
|
.Take(10)
|
||||||
|
.ToArrayAsyncLinqToDB();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyCollection<UserXpStats>> GetTopUserXps(ulong guildId, int count)
|
public async Task<IReadOnlyCollection<UserXpStats>> GetGuildUserXps(ulong guildId, List<ulong> users, int page)
|
||||||
{
|
{
|
||||||
await using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
return await uow.Set<UserXpStats>().GetTopUserXps(guildId, count);
|
return await uow.Set<UserXpStats>()
|
||||||
|
.Where(x => x.GuildId == guildId && x.UserId.In(users))
|
||||||
|
.OrderByDescending(x => x.Xp + x.AwardedXp)
|
||||||
|
.Skip(page * 10)
|
||||||
|
.Take(10)
|
||||||
|
.ToArrayAsyncLinqToDB();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<IReadOnlyCollection<DiscordUser>> GetUserXps(int page, int perPage = 9)
|
public async Task<IReadOnlyCollection<DiscordUser>> GetGlobalUserXps(int page)
|
||||||
{
|
{
|
||||||
using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
return uow.Set<DiscordUser>()
|
|
||||||
.GetUsersXpLeaderboardFor(page, perPage);
|
return await uow.GetTable<DiscordUser>()
|
||||||
|
.OrderByDescending(x => x.TotalXp)
|
||||||
|
.Skip(page * 10)
|
||||||
|
.Take(10)
|
||||||
|
.ToArrayAsyncLinqToDB();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyCollection<DiscordUser>> GetGlobalUserXps(int page, List<ulong> users)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
|
return await uow.GetTable<DiscordUser>()
|
||||||
|
.Where(x => x.UserId.In(users))
|
||||||
|
.OrderByDescending(x => x.TotalXp)
|
||||||
|
.Skip(page * 10)
|
||||||
|
.Take(10)
|
||||||
|
.ToArrayAsyncLinqToDB();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ChangeNotificationType(ulong userId, ulong guildId, XpNotificationLocation type)
|
public async Task ChangeNotificationType(ulong userId, ulong guildId, XpNotificationLocation type)
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>true</ImplicitUsings>
|
<ImplicitUsings>true</ImplicitUsings>
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
<Version>5.1.8</Version>
|
<Version>5.1.16</Version>
|
||||||
|
|
||||||
<!-- Output/build -->
|
<!-- Output/build -->
|
||||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||||
@@ -34,13 +34,12 @@
|
|||||||
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138"/>
|
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138"/>
|
||||||
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414"/>
|
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414"/>
|
||||||
<PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084"/>
|
<PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084"/>
|
||||||
<!-- <PackageReference Include="Grpc.AspNetCore" Version="2.62.0" />-->
|
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.26.1"/>
|
<PackageReference Include="Google.Protobuf" Version="3.28.2" />
|
||||||
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.62.0"/>
|
<PackageReference Include="Grpc" Version="2.46.6" />
|
||||||
<PackageReference Include="Grpc.Tools" Version="2.63.0">
|
<PackageReference Include="Grpc.Net.Client" Version="2.62.0" />
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PackageReference Include="Grpc.Tools" Version="2.66.0" PrivateAssets="All" />
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.5.0"/>
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.5.0"/>
|
||||||
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
|
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
|
||||||
@@ -69,19 +68,19 @@
|
|||||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/>
|
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/>
|
||||||
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1"/>
|
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1"/>
|
||||||
|
|
||||||
<PackageReference Include="SixLabors.Fonts" Version="2.0.4" />
|
<PackageReference Include="SixLabors.Fonts" Version="2.0.4"/>
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5"/>
|
||||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4" />
|
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4"/>
|
||||||
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009"/>
|
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009"/>
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.8.0" />
|
<PackageReference Include="StackExchange.Redis" Version="2.8.0"/>
|
||||||
<PackageReference Include="YamlDotNet" Version="15.1.4"/>
|
<PackageReference Include="YamlDotNet" Version="15.1.4"/>
|
||||||
<PackageReference Include="SharpToken" Version="2.0.3" />
|
<PackageReference Include="SharpToken" Version="2.0.3"/>
|
||||||
|
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0"/>
|
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0"/>
|
||||||
|
|
||||||
|
|
||||||
<!-- Db-related packages -->
|
<!-- Db-related packages -->
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8"/>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
@@ -89,7 +88,7 @@
|
|||||||
|
|
||||||
<PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0"/>
|
<PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0"/>
|
||||||
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8"/>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/>
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/>
|
||||||
|
|
||||||
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/>
|
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/>
|
||||||
@@ -103,20 +102,17 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\NadekoBot.GrpcApiBase\NadekoBot.GrpcApiBase.csproj"/>
|
||||||
<ProjectReference Include="..\Nadeko.Medusa\Nadeko.Medusa.csproj"/>
|
<ProjectReference Include="..\Nadeko.Medusa\Nadeko.Medusa.csproj"/>
|
||||||
<ProjectReference Include="..\NadekoBot.Voice\NadekoBot.Voice.csproj"/>
|
<ProjectReference Include="..\NadekoBot.Voice\NadekoBot.Voice.csproj"/>
|
||||||
<ProjectReference Include="..\NadekoBot.Generators\NadekoBot.Generators.csproj" OutputItemType="Analyzer"/>
|
<ProjectReference Include="..\NadekoBot.Generators\NadekoBot.Generators.csproj" OutputItemType="Analyzer"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AdditionalFiles Include="data\strings\responses\responses.en-US.json"/>
|
<AdditionalFiles Include="data\strings\responses\responses.en-US.json"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Protobuf Include="..\NadekoBot.Coordinator\Protos\coordinator.proto" GrpcServices="Client">
|
|
||||||
<Link>Protos\coordinator.proto</Link>
|
|
||||||
</Protobuf>
|
|
||||||
<None Update="data\**\*">
|
<None Update="data\**\*">
|
||||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
@@ -131,6 +127,13 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Protobuf Include="..\NadekoBot.Coordinator\Protos\coordinator.proto">
|
||||||
|
<Link>_common\CoordinatorProtos\coordinator.proto</Link>
|
||||||
|
<!-- <GrpcServices>Client</GrpcServices>-->
|
||||||
|
</Protobuf>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'GlobalNadeko' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'GlobalNadeko' ">
|
||||||
<!-- Define trace doesn't seem to affect the build at all so I had to remove $(DefineConstants)-->
|
<!-- Define trace doesn't seem to affect the build at all so I had to remove $(DefineConstants)-->
|
||||||
<DefineTrace>false</DefineTrace>
|
<DefineTrace>false</DefineTrace>
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
var pid = Environment.ProcessId;
|
|
||||||
|
|
||||||
var shardId = 0;
|
var shardId = 0;
|
||||||
int? totalShards = null; // 0 to read from creds.yml
|
int? totalShards = null; // 0 to read from creds.yml
|
||||||
if (args.Length > 0 && args[0] != "run")
|
if (args.Length > 0 && args[0] != "run")
|
||||||
@@ -22,7 +20,5 @@ if (args.Length > 0 && args[0] != "run")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LogSetup.SetupLogger(shardId);
|
|
||||||
Log.Information("Pid: {ProcessId}", pid);
|
|
||||||
|
|
||||||
await new Bot(shardId, totalShards, Environment.GetEnvironmentVariable("NadekoBot__creds")).RunAndBlockAsync();
|
await new Bot(shardId, totalShards, Environment.GetEnvironmentVariable("NadekoBot__creds")).RunAndBlockAsync();
|
149
src/NadekoBot/Services/GrpcApi/ExprsSvc.cs
Normal file
149
src/NadekoBot/Services/GrpcApi/ExprsSvc.cs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
using Grpc.Core;
|
||||||
|
using NadekoBot.Db.Models;
|
||||||
|
using NadekoBot.Modules.NadekoExpressions;
|
||||||
|
using NadekoBot.Modules.Utility;
|
||||||
|
|
||||||
|
namespace NadekoBot.GrpcApi;
|
||||||
|
|
||||||
|
public class ExprsSvc : GrpcExprs.GrpcExprsBase, IGrpcSvc, INService
|
||||||
|
{
|
||||||
|
private readonly NadekoExpressionsService _svc;
|
||||||
|
private readonly IQuoteService _qs;
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
|
||||||
|
public ExprsSvc(NadekoExpressionsService svc, IQuoteService qs, DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
_svc = svc;
|
||||||
|
_qs = qs;
|
||||||
|
_client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerServiceDefinition Bind()
|
||||||
|
=> GrpcExprs.BindService(this);
|
||||||
|
|
||||||
|
public override async Task<AddExprReply> AddExpr(AddExprRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Expr.Trigger) || string.IsNullOrWhiteSpace(request.Expr.Response))
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Trigger and response are required"));
|
||||||
|
|
||||||
|
NadekoExpression expr;
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.Expr.Id))
|
||||||
|
{
|
||||||
|
expr = await _svc.EditAsync(request.GuildId,
|
||||||
|
new kwum(request.Expr.Id),
|
||||||
|
request.Expr.Response,
|
||||||
|
request.Expr.Ca,
|
||||||
|
request.Expr.Ad,
|
||||||
|
request.Expr.Dm);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
expr = await _svc.AddAsync(request.GuildId,
|
||||||
|
request.Expr.Trigger,
|
||||||
|
request.Expr.Response,
|
||||||
|
request.Expr.Ca,
|
||||||
|
request.Expr.Ad,
|
||||||
|
request.Expr.Dm);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return new AddExprReply()
|
||||||
|
{
|
||||||
|
Id = new kwum(expr.Id).ToString(),
|
||||||
|
Success = true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetExprsReply> GetExprs(GetExprsRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var (exprs, totalCount) = await _svc.FindExpressionsAsync(request.GuildId, request.Query, request.Page);
|
||||||
|
|
||||||
|
var reply = new GetExprsReply();
|
||||||
|
reply.TotalCount = totalCount;
|
||||||
|
reply.Expressions.AddRange(exprs.Select(x => new ExprDto()
|
||||||
|
{
|
||||||
|
Ad = x.AutoDeleteTrigger,
|
||||||
|
At = x.AllowTarget,
|
||||||
|
Ca = x.ContainsAnywhere,
|
||||||
|
Dm = x.DmResponse,
|
||||||
|
Response = x.Response,
|
||||||
|
Id = new kwum(x.Id).ToString(),
|
||||||
|
Trigger = x.Trigger,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<Empty> DeleteExpr(DeleteExprRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (kwum.TryParse(request.Id, out var id))
|
||||||
|
await _svc.DeleteAsync(request.GuildId, id);
|
||||||
|
|
||||||
|
return new Empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetQuotesReply> GetQuotes(GetQuotesRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (request.Page < 0)
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Page must be >= 0"));
|
||||||
|
|
||||||
|
var (quotes, totalCount) = await _qs.FindQuotesAsync(request.GuildId, request.Query, request.Page);
|
||||||
|
|
||||||
|
var reply = new GetQuotesReply();
|
||||||
|
reply.TotalCount = totalCount;
|
||||||
|
reply.Quotes.AddRange(quotes.Select(x => new QuoteDto()
|
||||||
|
{
|
||||||
|
Id = new kwum(x.Id).ToString(),
|
||||||
|
Trigger = x.Keyword,
|
||||||
|
Response = x.Text,
|
||||||
|
AuthorId = x.AuthorId,
|
||||||
|
AuthorName = x.AuthorName
|
||||||
|
}));
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<AddQuoteReply> AddQuote(AddQuoteRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var userId = context.RequestHeaders.GetUserId();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Quote.Trigger) || string.IsNullOrWhiteSpace(request.Quote.Response))
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Trigger and response are required"));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Quote.Id))
|
||||||
|
{
|
||||||
|
var q = await _qs.AddQuoteAsync(request.GuildId,
|
||||||
|
userId,
|
||||||
|
(await _client.GetUserAsync(userId))?.Username ?? userId.ToString(),
|
||||||
|
request.Quote.Trigger,
|
||||||
|
request.Quote.Response);
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = new kwum(q.Id).ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kwum.TryParse(request.Quote.Id, out var qid))
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid quote id"));
|
||||||
|
|
||||||
|
await _qs.EditQuoteAsync(
|
||||||
|
request.GuildId,
|
||||||
|
new kwum(request.Quote.Id),
|
||||||
|
request.Quote.Trigger,
|
||||||
|
request.Quote.Response);
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = new kwum(qid).ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override async Task<Empty> DeleteQuote(DeleteQuoteRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
await _qs.DeleteQuoteAsync(request.GuildId, context.RequestHeaders.GetUserId(), true, new kwum(request.Id));
|
||||||
|
return new Empty();
|
||||||
|
}
|
||||||
|
}
|
89
src/NadekoBot/Services/GrpcApi/FinSvc.cs
Normal file
89
src/NadekoBot/Services/GrpcApi/FinSvc.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
using Grpc.Core;
|
||||||
|
using NadekoBot.Db.Models;
|
||||||
|
using NadekoBot.Modules.Gambling.Bank;
|
||||||
|
using NadekoBot.Modules.NadekoExpressions;
|
||||||
|
using NadekoBot.Modules.Utility;
|
||||||
|
|
||||||
|
namespace NadekoBot.GrpcApi;
|
||||||
|
|
||||||
|
public class FinSvc : GrpcFin.GrpcFinBase, IGrpcSvc, INService
|
||||||
|
{
|
||||||
|
private readonly ICurrencyService _cs;
|
||||||
|
private readonly IBankService _bank;
|
||||||
|
|
||||||
|
public FinSvc(ICurrencyService cs, IBankService bank)
|
||||||
|
{
|
||||||
|
_cs = cs;
|
||||||
|
_bank = bank;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerServiceDefinition Bind()
|
||||||
|
=> GrpcFin.BindService(this);
|
||||||
|
|
||||||
|
[GrpcNoAuthRequired]
|
||||||
|
public override async Task<DepositReply> Deposit(DepositRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (request.Amount <= 0)
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Amount must be greater than 0"));
|
||||||
|
|
||||||
|
var succ = await _bank.DepositAsync(request.UserId, request.Amount);
|
||||||
|
|
||||||
|
return new DepositReply
|
||||||
|
{
|
||||||
|
Success = succ
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[GrpcNoAuthRequired]
|
||||||
|
public override async Task<WithdrawReply> Withdraw(WithdrawRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (request.Amount <= 0)
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Amount must be greater than 0"));
|
||||||
|
|
||||||
|
var succ = await _bank.WithdrawAsync(request.UserId, request.Amount);
|
||||||
|
|
||||||
|
return new WithdrawReply
|
||||||
|
{
|
||||||
|
Success = succ
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[GrpcNoAuthRequired]
|
||||||
|
public override async Task<GetHoldingsReply> GetHoldings(GetHoldingsRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
return new GetHoldingsReply
|
||||||
|
{
|
||||||
|
Bank = await _bank.GetBalanceAsync(request.UserId),
|
||||||
|
Cash = await _cs.GetBalanceAsync(request.UserId)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[GrpcNoAuthRequired]
|
||||||
|
public override async Task<GetTransactionsReply> GetTransactions(
|
||||||
|
GetTransactionsRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (request.Page < 1)
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Page must be greater than 0"));
|
||||||
|
|
||||||
|
var trs = await _cs.GetTransactionsAsync(request.UserId, request.Page - 1);
|
||||||
|
|
||||||
|
var reply = new GetTransactionsReply
|
||||||
|
{
|
||||||
|
Total = await _cs.GetTransactionsCountAsync(request.UserId)
|
||||||
|
};
|
||||||
|
|
||||||
|
reply.Transactions.AddRange(trs.Select(x => new TransactionReply()
|
||||||
|
{
|
||||||
|
Id = new kwum(x.Id).ToString(),
|
||||||
|
Timestamp = Timestamp.FromDateTime(DateTime.UtcNow),
|
||||||
|
Amount = x.Amount,
|
||||||
|
Extra = x.Extra ?? string.Empty,
|
||||||
|
Note = x.Note ?? string.Empty,
|
||||||
|
Type = x.Type ?? string.Empty,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
}
|
117
src/NadekoBot/Services/GrpcApi/GreetByeSvc.cs
Normal file
117
src/NadekoBot/Services/GrpcApi/GreetByeSvc.cs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
using GreetType = NadekoBot.Services.GreetType;
|
||||||
|
|
||||||
|
namespace NadekoBot.GrpcApi;
|
||||||
|
|
||||||
|
public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, IGrpcSvc, INService
|
||||||
|
{
|
||||||
|
private readonly GreetService _gs;
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
|
||||||
|
public GreetByeSvc(GreetService gs, DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
_gs = gs;
|
||||||
|
_client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerServiceDefinition Bind()
|
||||||
|
=> GrpcGreet.BindService(this);
|
||||||
|
|
||||||
|
private static GrpcGreetSettings ToConf(GreetSettings? conf)
|
||||||
|
{
|
||||||
|
if (conf is null)
|
||||||
|
return new GrpcGreetSettings();
|
||||||
|
|
||||||
|
return new GrpcGreetSettings()
|
||||||
|
{
|
||||||
|
Message = conf.MessageText,
|
||||||
|
Type = (GrpcGreetType)conf.GreetType,
|
||||||
|
ChannelId = conf.ChannelId?.ToString() ?? string.Empty,
|
||||||
|
IsEnabled = conf.IsEnabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GrpcGreetSettings> GetGreetSettings(GetGreetRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var guildId = request.GuildId;
|
||||||
|
|
||||||
|
var conf = await _gs.GetGreetSettingsAsync(guildId, (GreetType)request.Type);
|
||||||
|
|
||||||
|
return ToConf(conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<UpdateGreetReply> UpdateGreet(UpdateGreetRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var gid = request.GuildId;
|
||||||
|
var s = request.Settings;
|
||||||
|
var msg = s.Message;
|
||||||
|
|
||||||
|
var type = GetGreetType(s.Type);
|
||||||
|
|
||||||
|
await _gs.SetMessage(gid, GetGreetType(s.Type), msg);
|
||||||
|
await _gs.SetGreet(gid, ulong.Parse(s.ChannelId), type, s.IsEnabled);
|
||||||
|
var settings = await _gs.GetGreetSettingsAsync(gid, type);
|
||||||
|
|
||||||
|
if (settings is null)
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Success = false
|
||||||
|
};
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Success = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<TestGreetReply> TestGreet(TestGreetRequest request, ServerCallContext context)
|
||||||
|
=> TestGreet(request.GuildId, request.ChannelId, request.UserId, request.Type);
|
||||||
|
|
||||||
|
private async Task<TestGreetReply> TestGreet(
|
||||||
|
ulong guildId,
|
||||||
|
ulong channelId,
|
||||||
|
ulong userId,
|
||||||
|
GrpcGreetType gtDto)
|
||||||
|
{
|
||||||
|
var g = _client.GetGuild(guildId) as IGuild;
|
||||||
|
if (g is null)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Error = "Guild doesn't exist",
|
||||||
|
Success = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var gu = await g.GetUserAsync(userId);
|
||||||
|
var ch = await g.GetTextChannelAsync(channelId);
|
||||||
|
|
||||||
|
if (gu is null || ch is null)
|
||||||
|
return new TestGreetReply()
|
||||||
|
{
|
||||||
|
Error = "Guild or channel doesn't exist",
|
||||||
|
Success = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var gt = GetGreetType(gtDto);
|
||||||
|
|
||||||
|
await _gs.Test(guildId, gt, ch, gu);
|
||||||
|
return new TestGreetReply()
|
||||||
|
{
|
||||||
|
Success = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GreetType GetGreetType(GrpcGreetType gtDto)
|
||||||
|
{
|
||||||
|
return gtDto switch
|
||||||
|
{
|
||||||
|
GrpcGreetType.Greet => GreetType.Greet,
|
||||||
|
GrpcGreetType.GreetDm => GreetType.GreetDm,
|
||||||
|
GrpcGreetType.Bye => GreetType.Bye,
|
||||||
|
GrpcGreetType.Boost => GreetType.Boost,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(gtDto), gtDto, null)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
95
src/NadekoBot/Services/GrpcApi/NCanvasSvc.cs
Normal file
95
src/NadekoBot/Services/GrpcApi/NCanvasSvc.cs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
using Grpc.Core;
|
||||||
|
using NadekoBot.Db.Models;
|
||||||
|
using NadekoBot.Modules.Games;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
|
namespace NadekoBot.GrpcApi;
|
||||||
|
|
||||||
|
public class NCanvasSvc : GrpcNCanvas.GrpcNCanvasBase, IGrpcSvc, INService
|
||||||
|
{
|
||||||
|
private readonly INCanvasService _nCanvas;
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
|
||||||
|
public NCanvasSvc(INCanvasService nCanvas, DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
_nCanvas = nCanvas;
|
||||||
|
_client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerServiceDefinition Bind()
|
||||||
|
=> GrpcNCanvas.BindService(this);
|
||||||
|
|
||||||
|
[GrpcNoAuthRequired]
|
||||||
|
public override async Task<CanvasReply> GetCanvas(Empty request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var pixels = await _nCanvas.GetCanvas();
|
||||||
|
var reply = new CanvasReply()
|
||||||
|
{
|
||||||
|
Width = _nCanvas.GetWidth(),
|
||||||
|
Height = _nCanvas.GetHeight()
|
||||||
|
};
|
||||||
|
reply.Pixels.AddRange(pixels);
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GrpcNoAuthRequired]
|
||||||
|
public override async Task<GetPixelReply> GetPixel(GetPixelRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var pixel = await _nCanvas.GetPixel(request.X, request.Y);
|
||||||
|
if (pixel is null)
|
||||||
|
throw new RpcException(new Status(StatusCode.NotFound, "Pixel not found"));
|
||||||
|
|
||||||
|
var reply = MapPixelToGrpcPixel(pixel);
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GetPixelReply MapPixelToGrpcPixel(NCPixel pixel)
|
||||||
|
{
|
||||||
|
var reply = new GetPixelReply
|
||||||
|
{
|
||||||
|
Color = "#" + new Rgba32(pixel.Color).ToHex(),
|
||||||
|
PackedColor = pixel.Color,
|
||||||
|
Position = new kwum(pixel.Position).ToString(),
|
||||||
|
PositionX = pixel.Position % _nCanvas.GetWidth(),
|
||||||
|
PositionY = pixel.Position / _nCanvas.GetWidth(),
|
||||||
|
// Owner = await ((IDiscordClient)_client).GetUserAsync(pixel.OwnerId)?.ToString() ?? string.Empty,
|
||||||
|
// OwnerId = pixel.OwnerId.ToString(),
|
||||||
|
Price = pixel.Price,
|
||||||
|
Text = pixel.Text
|
||||||
|
};
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GrpcNoAuthRequired]
|
||||||
|
public override async Task<SetPixelReply> SetPixel(SetPixelRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (!kwum.TryParse(request.Position, out var pos))
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Position is invalid"));
|
||||||
|
|
||||||
|
if (!Rgba32.TryParseHex(request.Color, out var clr))
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Color is invalid"));
|
||||||
|
|
||||||
|
var userId = context.RequestHeaders.GetUserId();
|
||||||
|
var result = await _nCanvas.SetPixel(pos, clr.PackedValue, request.Text, userId, request.Price);
|
||||||
|
var reply = new SetPixelReply()
|
||||||
|
{
|
||||||
|
Success = result == SetPixelResult.Success,
|
||||||
|
Error = result switch
|
||||||
|
{
|
||||||
|
SetPixelResult.Success => string.Empty,
|
||||||
|
SetPixelResult.InsufficientPayment => "You have to pay equal or more than the price.",
|
||||||
|
SetPixelResult.NotEnoughMoney => "You don't have enough currency. ",
|
||||||
|
SetPixelResult.InvalidInput =>
|
||||||
|
$"Invalid input. Position has to be >= 0 and < {_nCanvas.GetWidth()}x{_nCanvas.GetHeight()}",
|
||||||
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var pixel = await _nCanvas.GetPixel(pos);
|
||||||
|
if (pixel is not null)
|
||||||
|
reply.Pixel = MapPixelToGrpcPixel(pixel);
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
}
|
253
src/NadekoBot/Services/GrpcApi/OtherSvc.cs
Normal file
253
src/NadekoBot/Services/GrpcApi/OtherSvc.cs
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
|
using Grpc.Core;
|
||||||
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
|
using NadekoBot.Modules.Xp.Services;
|
||||||
|
|
||||||
|
namespace NadekoBot.GrpcApi;
|
||||||
|
|
||||||
|
public static class GrpcApiExtensions
|
||||||
|
{
|
||||||
|
public static ulong GetUserId(this ServerCallContext context)
|
||||||
|
=> ulong.Parse(context.RequestHeaders.FirstOrDefault(x => x.Key == "userid")!.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class OtherSvc : GrpcOther.GrpcOtherBase, IGrpcSvc, INService
|
||||||
|
{
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
private readonly XpService _xp;
|
||||||
|
private readonly ICurrencyService _cur;
|
||||||
|
private readonly WaifuService _waifus;
|
||||||
|
private readonly IStatsService _stats;
|
||||||
|
private readonly CommandHandler _cmdHandler;
|
||||||
|
|
||||||
|
public OtherSvc(
|
||||||
|
DiscordSocketClient client,
|
||||||
|
XpService xp,
|
||||||
|
ICurrencyService cur,
|
||||||
|
WaifuService waifus,
|
||||||
|
IStatsService stats,
|
||||||
|
CommandHandler cmdHandler)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_xp = xp;
|
||||||
|
_cur = cur;
|
||||||
|
_waifus = waifus;
|
||||||
|
_stats = stats;
|
||||||
|
_cmdHandler = cmdHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerServiceDefinition Bind()
|
||||||
|
=> GrpcOther.BindService(this);
|
||||||
|
|
||||||
|
[GrpcNoAuthRequired]
|
||||||
|
public override Task<BotOnGuildReply> BotOnGuild(BotOnGuildRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var guild = _client.GetGuild(request.GuildId);
|
||||||
|
|
||||||
|
var reply = new BotOnGuildReply
|
||||||
|
{
|
||||||
|
Success = guild is not null
|
||||||
|
};
|
||||||
|
|
||||||
|
return Task.FromResult(reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<GetRolesReply> GetRoles(GetRolesRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var g = _client.GetGuild(request.GuildId);
|
||||||
|
var roles = g?.Roles;
|
||||||
|
var reply = new GetRolesReply();
|
||||||
|
reply.Roles.AddRange(roles?.Select(x => new RoleReply()
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
Name = x.Name,
|
||||||
|
Color = x.Color.ToString(),
|
||||||
|
IconUrl = x.GetIconUrl() ?? string.Empty,
|
||||||
|
})
|
||||||
|
?? new List<RoleReply>());
|
||||||
|
|
||||||
|
return Task.FromResult(reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetTextChannelsReply> GetTextChannels(
|
||||||
|
GetTextChannelsRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
IGuild g = _client.GetGuild(request.GuildId);
|
||||||
|
var reply = new GetTextChannelsReply();
|
||||||
|
|
||||||
|
var chs = await g.GetTextChannelsAsync();
|
||||||
|
|
||||||
|
reply.TextChannels.AddRange(chs.Select(x => new TextChannelReply()
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
Name = x.Name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[GrpcNoAuthRequired]
|
||||||
|
public override async Task<CurrencyLbReply> GetCurrencyLb(GetLbRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var users = await _cur.GetTopRichest(_client.CurrentUser.Id, request.Page, request.PerPage);
|
||||||
|
|
||||||
|
var reply = new CurrencyLbReply();
|
||||||
|
var entries = users.Select(x =>
|
||||||
|
{
|
||||||
|
var user = _client.GetUser(x.UserId);
|
||||||
|
return Task.FromResult(new CurrencyLbEntryReply()
|
||||||
|
{
|
||||||
|
Amount = x.CurrencyAmount,
|
||||||
|
User = user?.ToString() ?? x.Username,
|
||||||
|
UserId = x.UserId,
|
||||||
|
Avatar = user?.RealAvatarUrl().ToString() ?? x.RealAvatarUrl()?.ToString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
reply.Entries.AddRange(await entries.WhenAll());
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GrpcNoAuthRequired]
|
||||||
|
public override async Task<XpLbReply> GetXpLb(GetLbRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var users = await _xp.GetGlobalUserXps(request.Page);
|
||||||
|
|
||||||
|
var reply = new XpLbReply();
|
||||||
|
|
||||||
|
var entries = users.Select(x =>
|
||||||
|
{
|
||||||
|
var lvl = new LevelStats(x.TotalXp);
|
||||||
|
|
||||||
|
return new XpLbEntryReply()
|
||||||
|
{
|
||||||
|
Level = lvl.Level,
|
||||||
|
TotalXp = x.TotalXp,
|
||||||
|
User = x.Username,
|
||||||
|
UserId = x.UserId
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
reply.Entries.AddRange(entries);
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GrpcNoAuthRequired]
|
||||||
|
public override async Task<WaifuLbReply> GetWaifuLb(GetLbRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var waifus = await _waifus.GetTopWaifusAtPage(request.Page, request.PerPage);
|
||||||
|
|
||||||
|
var reply = new WaifuLbReply();
|
||||||
|
reply.Entries.AddRange(waifus.Select(x => new WaifuLbEntry()
|
||||||
|
{
|
||||||
|
ClaimedBy = x.ClaimerName ?? string.Empty,
|
||||||
|
IsMutual = x.ClaimerName == x.Affinity,
|
||||||
|
Value = x.Price,
|
||||||
|
User = x.WaifuName,
|
||||||
|
}));
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GrpcNoAuthRequired]
|
||||||
|
public override async Task GetShardStats(
|
||||||
|
Empty request,
|
||||||
|
IServerStreamWriter<ShardStatsReply> responseStream,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var stats = new ShardStatsReply()
|
||||||
|
{
|
||||||
|
Id = _client.ShardId,
|
||||||
|
Commands = _stats.CommandsRan,
|
||||||
|
Uptime = _stats.GetUptimeString(),
|
||||||
|
Status = GetConnectionState(_client.ConnectionState),
|
||||||
|
GuildCount = _client.Guilds.Count,
|
||||||
|
};
|
||||||
|
|
||||||
|
await responseStream.WriteAsync(stats);
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[GrpcNoAuthRequired]
|
||||||
|
public override async Task GetCommandFeed(
|
||||||
|
Empty request,
|
||||||
|
IServerStreamWriter<CommandFeedEntry> responseStream,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
var taskCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
Task OnCommandExecuted(IUserMessage userMessage, CommandInfo commandInfo)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
responseStream.WriteAsync(new()
|
||||||
|
{
|
||||||
|
Command = commandInfo.Name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_cmdHandler.CommandExecuted -= OnCommandExecuted;
|
||||||
|
taskCompletion.TrySetResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cmdHandler.CommandExecuted += OnCommandExecuted;
|
||||||
|
|
||||||
|
await taskCompletion.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetConnectionState(ConnectionState clientConnectionState)
|
||||||
|
{
|
||||||
|
return clientConnectionState switch
|
||||||
|
{
|
||||||
|
ConnectionState.Connected => "Connected",
|
||||||
|
ConnectionState.Connecting => "Connecting",
|
||||||
|
_ => "Disconnected"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetServerInfoReply> GetServerInfo(ServerInfoRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var info = await _stats.GetGuildInfoAsync(request.GuildId);
|
||||||
|
|
||||||
|
var reply = new GetServerInfoReply()
|
||||||
|
{
|
||||||
|
Id = info.Id,
|
||||||
|
Name = info.Name,
|
||||||
|
IconUrl = info.IconUrl,
|
||||||
|
OwnerId = info.OwnerId,
|
||||||
|
OwnerName = info.Owner,
|
||||||
|
TextChannels = info.TextChannels,
|
||||||
|
VoiceChannels = info.VoiceChannels,
|
||||||
|
MemberCount = info.MemberCount,
|
||||||
|
CreatedAt = info.CreatedAt.Ticks,
|
||||||
|
};
|
||||||
|
|
||||||
|
reply.Features.AddRange(info.Features);
|
||||||
|
reply.Emojis.AddRange(info.Emojis.Select(x => new EmojiReply()
|
||||||
|
{
|
||||||
|
Name = x.Name,
|
||||||
|
Url = x.Url,
|
||||||
|
Code = x.ToString()
|
||||||
|
}));
|
||||||
|
|
||||||
|
reply.Roles.AddRange(info.Roles.Select(x => new RoleReply()
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
Name = x.Name,
|
||||||
|
IconUrl = x.GetIconUrl() ?? string.Empty,
|
||||||
|
Color = x.Color.ToString()
|
||||||
|
}));
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
}
|
224
src/NadekoBot/Services/GrpcApi/WarnSvc.cs
Normal file
224
src/NadekoBot/Services/GrpcApi/WarnSvc.cs
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
using NadekoBot.Db.Models;
|
||||||
|
using NadekoBot.Modules.Administration.Services;
|
||||||
|
using Enum = System.Enum;
|
||||||
|
|
||||||
|
namespace NadekoBot.GrpcApi;
|
||||||
|
|
||||||
|
public sealed class WarnSvc : GrpcWarn.GrpcWarnBase, IGrpcSvc, INService
|
||||||
|
{
|
||||||
|
private readonly UserPunishService _ups;
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
|
||||||
|
public WarnSvc(UserPunishService ups, DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
_ups = ups;
|
||||||
|
_client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerServiceDefinition Bind()
|
||||||
|
=> GrpcWarn.BindService(this);
|
||||||
|
|
||||||
|
public override async Task<WarnSettingsReply> GetWarnSettings(
|
||||||
|
WarnSettingsRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
var list = await _ups.WarnPunishList(request.GuildId);
|
||||||
|
|
||||||
|
var wsr = new WarnSettingsReply();
|
||||||
|
|
||||||
|
(wsr.ExpiryDays, wsr.DeleteOnExpire) = await _ups.GetWarnExpire(request.GuildId);
|
||||||
|
|
||||||
|
wsr.Punishments.AddRange(list.Select(x => new WarnPunishment()
|
||||||
|
{
|
||||||
|
Action = x.Punishment.ToString(),
|
||||||
|
Duration = x.Time,
|
||||||
|
Threshold = x.Count,
|
||||||
|
Role = x.RoleId is ulong rid
|
||||||
|
? _client.GetGuild(request.GuildId)?.GetRole(rid)?.Name ?? x.RoleId?.ToString() ?? string.Empty
|
||||||
|
: string.Empty
|
||||||
|
}));
|
||||||
|
|
||||||
|
return wsr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<SetWarnExpiryReply> SetWarnExpiry(
|
||||||
|
SetWarnExpiryRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (request.ExpiryDays > 366)
|
||||||
|
{
|
||||||
|
return new SetWarnExpiryReply()
|
||||||
|
{
|
||||||
|
Success = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await _ups.WarnExpireAsync(request.GuildId, request.ExpiryDays, request.DeleteOnExpire);
|
||||||
|
|
||||||
|
return new SetWarnExpiryReply()
|
||||||
|
{
|
||||||
|
Success = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<DeleteWarnpReply> DeleteWarnp(DeleteWarnpRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
var succ = await _ups.WarnPunishRemove(request.GuildId, request.Threshold);
|
||||||
|
|
||||||
|
return new DeleteWarnpReply
|
||||||
|
{
|
||||||
|
Success = succ
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<AddWarnpReply> AddWarnp(AddWarnpRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (request.Punishment.Threshold <= 0)
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Threshold must be greater than 0"));
|
||||||
|
|
||||||
|
var g = _client.GetGuild(request.GuildId);
|
||||||
|
|
||||||
|
if (g is null)
|
||||||
|
throw new RpcException(new Status(StatusCode.NotFound, "Guild not found"));
|
||||||
|
|
||||||
|
if (!Enum.TryParse<PunishmentAction>(request.Punishment.Action, out var action))
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid action"));
|
||||||
|
|
||||||
|
IRole? role = null;
|
||||||
|
if (action == PunishmentAction.AddRole && ulong.TryParse(request.Punishment.Role, out var roleId))
|
||||||
|
{
|
||||||
|
role = g.GetRole(roleId);
|
||||||
|
|
||||||
|
if (role is null)
|
||||||
|
return new AddWarnpReply()
|
||||||
|
{
|
||||||
|
Success = false
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!ulong.TryParse(context.RequestHeaders.GetValue("userid"), out var userId))
|
||||||
|
return new AddWarnpReply()
|
||||||
|
{
|
||||||
|
Success = false
|
||||||
|
};
|
||||||
|
|
||||||
|
var user = await ((IGuild)g).GetUserAsync(userId);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
throw new RpcException(new Status(StatusCode.NotFound, "User not found"));
|
||||||
|
|
||||||
|
var userMaxRole = user.GetRoles().MaxBy(x => x.Position)?.Position ?? 0;
|
||||||
|
if (g.OwnerId != user.Id && userMaxRole <= role.Position)
|
||||||
|
{
|
||||||
|
return new AddWarnpReply()
|
||||||
|
{
|
||||||
|
Success = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var duration = TimeSpan.FromMinutes(request.Punishment.Duration);
|
||||||
|
|
||||||
|
var succ = await _ups.WarnPunish(request.GuildId,
|
||||||
|
request.Punishment.Threshold,
|
||||||
|
action,
|
||||||
|
duration,
|
||||||
|
role
|
||||||
|
);
|
||||||
|
|
||||||
|
return new AddWarnpReply()
|
||||||
|
{
|
||||||
|
Success = succ
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetLatestWarningsReply> GetLatestWarnings(
|
||||||
|
GetLatestWarningsRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
var (latest, count) = await _ups.GetLatestWarnings(request.GuildId, request.Page);
|
||||||
|
|
||||||
|
var reply = new GetLatestWarningsReply()
|
||||||
|
{
|
||||||
|
TotalCount = count
|
||||||
|
};
|
||||||
|
|
||||||
|
reply.Warnings.AddRange(latest.Select(MapWarningToGrpcWarning));
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetUserWarningsReply> GetUserWarnings(
|
||||||
|
GetUserWarningsRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
IReadOnlyCollection<Db.Models.Warning> latest = [];
|
||||||
|
var count = 0;
|
||||||
|
if (ulong.TryParse(request.User, out var userId))
|
||||||
|
{
|
||||||
|
(latest, count) = await _ups.GetUserWarnings(request.GuildId, userId, request.Page);
|
||||||
|
}
|
||||||
|
else if (_client.GetGuild(request.GuildId)?.Users.FirstOrDefault(x => x.Username == request.User) is { } user)
|
||||||
|
{
|
||||||
|
(latest, count) = await _ups.GetUserWarnings(request.GuildId, user.Id, request.Page);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
var reply = new GetUserWarningsReply
|
||||||
|
{
|
||||||
|
TotalCount = count
|
||||||
|
};
|
||||||
|
|
||||||
|
reply.Warnings.AddRange(latest.Select(MapWarningToGrpcWarning));
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Warning MapWarningToGrpcWarning(Db.Models.Warning x)
|
||||||
|
{
|
||||||
|
return new Warning
|
||||||
|
{
|
||||||
|
Id = new kwum(x.Id).ToString(),
|
||||||
|
Forgiven = x.Forgiven,
|
||||||
|
ForgivenBy = x.ForgivenBy ?? string.Empty,
|
||||||
|
Reason = x.Reason ?? string.Empty,
|
||||||
|
Timestamp = x.DateAdded is { } da ? Nadeko.Common.Extensions.ToTimestamp(da) : 0,
|
||||||
|
Weight = x.Weight,
|
||||||
|
Moderator = x.Moderator ?? string.Empty,
|
||||||
|
User = _client.GetUser(x.UserId)?.Username ?? x.UserId.ToString(),
|
||||||
|
UserId = x.UserId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<ForgiveWarningReply> ForgiveWarning(
|
||||||
|
ForgiveWarningRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (!kwum.TryParse(request.WarnId, out var wid))
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid warning ID"));
|
||||||
|
|
||||||
|
var succ = await _ups.ForgiveWarning(request.GuildId, wid, request.ModName);
|
||||||
|
|
||||||
|
return new ForgiveWarningReply
|
||||||
|
{
|
||||||
|
Success = succ
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<ForgiveWarningReply> DeleteWarning(
|
||||||
|
ForgiveWarningRequest request,
|
||||||
|
ServerCallContext context)
|
||||||
|
{
|
||||||
|
if (!kwum.TryParse(request.WarnId, out var wid))
|
||||||
|
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid warning ID"));
|
||||||
|
|
||||||
|
var succ = await _ups.WarnDelete(request.GuildId, wid);
|
||||||
|
|
||||||
|
return new ForgiveWarningReply
|
||||||
|
{
|
||||||
|
Success = succ
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
119
src/NadekoBot/Services/GrpcApiPermsInterceptor.cs
Normal file
119
src/NadekoBot/Services/GrpcApiPermsInterceptor.cs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
using Grpc.Core.Interceptors;
|
||||||
|
|
||||||
|
namespace NadekoBot.GrpcApi;
|
||||||
|
|
||||||
|
public sealed partial class GrpcApiPermsInterceptor : Interceptor
|
||||||
|
{
|
||||||
|
private const GuildPerm DEFAULT_PERMISSION = GuildPermission.Administrator;
|
||||||
|
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
|
||||||
|
public GrpcApiPermsInterceptor(DiscordSocketClient client)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RequestHandler(ServerCallContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var method = context.Method[(context.Method.LastIndexOf('/') + 1)..];
|
||||||
|
|
||||||
|
// get metadata
|
||||||
|
var metadata = context
|
||||||
|
.RequestHeaders
|
||||||
|
.ToDictionary(x => x.Key, x => x.Value);
|
||||||
|
|
||||||
|
Log.Information("grpc | g: {GuildId} | u: {UserID} | cmd: {Method}",
|
||||||
|
metadata.TryGetValue("guildid", out var gidString) ? gidString : "none",
|
||||||
|
metadata.TryGetValue("userid", out var uidString) ? uidString : "none",
|
||||||
|
method);
|
||||||
|
|
||||||
|
|
||||||
|
// there always has to be a user who makes the call
|
||||||
|
if (!metadata.ContainsKey("userid"))
|
||||||
|
throw new RpcException(new(StatusCode.Unauthenticated, "userid has to be specified."));
|
||||||
|
|
||||||
|
// get the method name without the service name
|
||||||
|
|
||||||
|
// if the method is explicitly marked as not requiring auth
|
||||||
|
if (_noAuthRequired.Contains(method))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// otherwise the method requires auth, and if it requires auth then the guildid has to be specified
|
||||||
|
if (string.IsNullOrWhiteSpace(gidString))
|
||||||
|
throw new RpcException(new(StatusCode.Unauthenticated, "guildid has to be specified."));
|
||||||
|
|
||||||
|
var userId = ulong.Parse(metadata["userid"]);
|
||||||
|
var guildId = ulong.Parse(gidString);
|
||||||
|
|
||||||
|
// check if the user has the required permission
|
||||||
|
if (_perms.TryGetValue(method, out var perm))
|
||||||
|
{
|
||||||
|
await EnsureUserHasPermission(guildId, userId, perm);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// if not then use the default, which is Administrator permission
|
||||||
|
await EnsureUserHasPermission(guildId, userId, DEFAULT_PERMISSION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Error thrown by {ContextMethod}", context.Method);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureUserHasPermission(ulong guildId, ulong userId, GuildPerm perm)
|
||||||
|
{
|
||||||
|
IGuild guild = _client.GetGuild(guildId);
|
||||||
|
var user = guild is null ? null : await guild.GetUserAsync(userId);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
throw new RpcException(new Status(StatusCode.NotFound, "User not found"));
|
||||||
|
|
||||||
|
if (!user.GuildPermissions.Has(perm))
|
||||||
|
throw new RpcException(new Status(StatusCode.PermissionDenied,
|
||||||
|
$"You need {perm} permission to use this method"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>(
|
||||||
|
IAsyncStreamReader<TRequest> requestStream,
|
||||||
|
ServerCallContext context,
|
||||||
|
ClientStreamingServerMethod<TRequest, TResponse> continuation)
|
||||||
|
{
|
||||||
|
await RequestHandler(context);
|
||||||
|
return await continuation(requestStream, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task DuplexStreamingServerHandler<TRequest, TResponse>(
|
||||||
|
IAsyncStreamReader<TRequest> requestStream,
|
||||||
|
IServerStreamWriter<TResponse> responseStream,
|
||||||
|
ServerCallContext context,
|
||||||
|
DuplexStreamingServerMethod<TRequest, TResponse> continuation)
|
||||||
|
{
|
||||||
|
await RequestHandler(context);
|
||||||
|
await continuation(requestStream, responseStream, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task ServerStreamingServerHandler<TRequest, TResponse>(
|
||||||
|
TRequest request,
|
||||||
|
IServerStreamWriter<TResponse> responseStream,
|
||||||
|
ServerCallContext context,
|
||||||
|
ServerStreamingServerMethod<TRequest, TResponse> continuation)
|
||||||
|
{
|
||||||
|
await RequestHandler(context);
|
||||||
|
await continuation(request, responseStream, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
|
||||||
|
TRequest request,
|
||||||
|
ServerCallContext context,
|
||||||
|
UnaryServerMethod<TRequest, TResponse> continuation)
|
||||||
|
{
|
||||||
|
await RequestHandler(context);
|
||||||
|
return await continuation(request, context);
|
||||||
|
}
|
||||||
|
}
|
75
src/NadekoBot/Services/GrpcApiService.cs
Normal file
75
src/NadekoBot/Services/GrpcApiService.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
using Grpc.Core.Interceptors;
|
||||||
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
|
|
||||||
|
namespace NadekoBot.GrpcApi;
|
||||||
|
|
||||||
|
public class GrpcApiService : INService, IReadyExecutor
|
||||||
|
{
|
||||||
|
private Server? _app;
|
||||||
|
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
private readonly IEnumerable<IGrpcSvc> _svcs;
|
||||||
|
private readonly IBotCredsProvider _creds;
|
||||||
|
|
||||||
|
public GrpcApiService(
|
||||||
|
DiscordSocketClient client,
|
||||||
|
IEnumerable<IGrpcSvc> svcs,
|
||||||
|
IBotCredsProvider creds)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_svcs = svcs;
|
||||||
|
_creds = creds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnReadyAsync()
|
||||||
|
{
|
||||||
|
var creds = _creds.GetCreds();
|
||||||
|
if (creds.GrpcApi is null || !creds.GrpcApi.Enabled)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var host = creds.GrpcApi.Host;
|
||||||
|
var port = creds.GrpcApi.Port + _client.ShardId;
|
||||||
|
|
||||||
|
var interceptor = new GrpcApiPermsInterceptor(_client);
|
||||||
|
|
||||||
|
var serverCreds = ServerCredentials.Insecure;
|
||||||
|
|
||||||
|
if (creds.GrpcApi is
|
||||||
|
{
|
||||||
|
CertPrivateKey: not null and not "",
|
||||||
|
CertChain: not null and not ""
|
||||||
|
} cert)
|
||||||
|
{
|
||||||
|
serverCreds = new SslServerCredentials(
|
||||||
|
new[] { new KeyCertificatePair(cert.CertChain, cert.CertPrivateKey) });
|
||||||
|
}
|
||||||
|
|
||||||
|
_app = new()
|
||||||
|
{
|
||||||
|
Ports =
|
||||||
|
{
|
||||||
|
new(host, port, serverCreds),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var svc in _svcs)
|
||||||
|
{
|
||||||
|
_app.Services.Add(svc.Bind().Intercept(interceptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
_app.Start();
|
||||||
|
|
||||||
|
Log.Information("Grpc Api Server started on port {Host}:{Port}", host, port);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "Error starting Grpc Api Server");
|
||||||
|
_app?.ShutdownAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
8
src/NadekoBot/Services/IGrpcSvc.cs
Normal file
8
src/NadekoBot/Services/IGrpcSvc.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
|
||||||
|
namespace NadekoBot.GrpcApi;
|
||||||
|
|
||||||
|
public interface IGrpcSvc
|
||||||
|
{
|
||||||
|
ServerServiceDefinition Bind();
|
||||||
|
}
|
9
src/NadekoBot/Services/SvcExtensions.cs
Normal file
9
src/NadekoBot/Services/SvcExtensions.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Grpc.Core;
|
||||||
|
|
||||||
|
namespace NadekoBot.GrpcApi;
|
||||||
|
|
||||||
|
public static class SvcExtensions
|
||||||
|
{
|
||||||
|
public static ulong GetUserId(this Metadata meta)
|
||||||
|
=> ulong.Parse(meta.FirstOrDefault(x => x.Key == "userid")!.Value);
|
||||||
|
}
|
@@ -6,9 +6,9 @@ namespace Nadeko.Common;
|
|||||||
|
|
||||||
public static class LogSetup
|
public static class LogSetup
|
||||||
{
|
{
|
||||||
public static void SetupLogger(object source)
|
public static void SetupLogger(object source, IBotCreds creds)
|
||||||
{
|
{
|
||||||
Log.Logger = new LoggerConfiguration().MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
var config = new LoggerConfiguration().MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
||||||
.MinimumLevel.Override("System", LogEventLevel.Information)
|
.MinimumLevel.Override("System", LogEventLevel.Information)
|
||||||
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
|
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
|
||||||
.Enrich.FromLogContext()
|
.Enrich.FromLogContext()
|
||||||
@@ -16,8 +16,13 @@ public static class LogSetup
|
|||||||
theme: GetTheme(),
|
theme: GetTheme(),
|
||||||
outputTemplate:
|
outputTemplate:
|
||||||
"[{Timestamp:HH:mm:ss} {Level:u3}] | #{LogSource} | {Message:lj}{NewLine}{Exception}")
|
"[{Timestamp:HH:mm:ss} {Level:u3}] | #{LogSource} | {Message:lj}{NewLine}{Exception}")
|
||||||
.Enrich.WithProperty("LogSource", source)
|
.Enrich.WithProperty("LogSource", source);
|
||||||
.CreateLogger();
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(creds.Seq.Url))
|
||||||
|
config = config.WriteTo.Seq(creds.Seq.Url, apiKey: creds.Seq.ApiKey);
|
||||||
|
|
||||||
|
Log.Logger = config
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
Console.OutputEncoding = Encoding.UTF8;
|
Console.OutputEncoding = Encoding.UTF8;
|
||||||
}
|
}
|
||||||
|
@@ -76,6 +76,9 @@ public readonly struct kwum : IEquatable<kwum>
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
if (_value == 0)
|
||||||
|
return VALID_CHARACTERS[0].ToString();
|
||||||
|
|
||||||
var count = VALID_CHARACTERS.Length;
|
var count = VALID_CHARACTERS.Length;
|
||||||
var localValue = _value;
|
var localValue = _value;
|
||||||
var arrSize = (int)Math.Log(localValue, count) + 1;
|
var arrSize = (int)Math.Log(localValue, count) + 1;
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
namespace NadekoBot;
|
namespace NadekoBot;
|
||||||
|
|
||||||
public interface IBotCredentials
|
public interface IBotCreds
|
||||||
{
|
{
|
||||||
string Token { get; }
|
string Token { get; }
|
||||||
string NadekoAiToken { get; }
|
string NadekoAiToken { get; }
|
||||||
@@ -29,6 +29,8 @@ public interface IBotCredentials
|
|||||||
string TwitchClientSecret { get; set; }
|
string TwitchClientSecret { get; set; }
|
||||||
GoogleApiConfig Google { get; set; }
|
GoogleApiConfig Google { get; set; }
|
||||||
BotCacheImplemenation BotCache { get; set; }
|
BotCacheImplemenation BotCache { get; set; }
|
||||||
|
Creds.GrpcApiConfig GrpcApi { get; set; }
|
||||||
|
SeqConfig Seq { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IVotesSettings
|
public interface IVotesSettings
|
@@ -3,6 +3,6 @@
|
|||||||
public interface IBotCredsProvider
|
public interface IBotCredsProvider
|
||||||
{
|
{
|
||||||
public void Reload();
|
public void Reload();
|
||||||
public IBotCredentials GetCreds();
|
public IBotCreds GetCreds();
|
||||||
public void ModifyCredsFile(Action<IBotCredentials> func);
|
public void ModifyCredsFile(Action<IBotCreds> func);
|
||||||
}
|
}
|
@@ -29,7 +29,7 @@ public sealed partial class BotConfig : ICloneable<BotConfig>
|
|||||||
public CultureInfo DefaultLocale { get; set; }
|
public CultureInfo DefaultLocale { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Style in which executed commands will show up in the console.
|
Style in which executed commands will show up in the logs.
|
||||||
Allowed values: Simple, Normal, None
|
Allowed values: Simple, Normal, None
|
||||||
""")]
|
""")]
|
||||||
public ConsoleOutputType ConsoleOutputType { get; set; }
|
public ConsoleOutputType ConsoleOutputType { get; set; }
|
||||||
|
@@ -3,30 +3,31 @@ using NadekoBot.Common.Yml;
|
|||||||
|
|
||||||
namespace NadekoBot.Common;
|
namespace NadekoBot.Common;
|
||||||
|
|
||||||
public sealed class Creds : IBotCredentials
|
public sealed class Creds : IBotCreds
|
||||||
{
|
{
|
||||||
[Comment("""DO NOT CHANGE""")]
|
[Comment("""DO NOT CHANGE""")]
|
||||||
public int Version { get; set; }
|
public int Version { get; set; } = 13;
|
||||||
|
|
||||||
[Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")]
|
[Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")]
|
||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
List of Ids of the users who have bot owner permissions
|
List of Ids of the users who have bot owner permissions
|
||||||
**DO NOT ADD PEOPLE YOU DON'T TRUST**
|
**DO NOT ADD PEOPLE YOU DON'T TRUST**
|
||||||
""")]
|
""")]
|
||||||
public ICollection<ulong> OwnerIds { get; set; }
|
public ICollection<ulong> OwnerIds { get; set; }
|
||||||
|
|
||||||
[Comment("Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted")]
|
[Comment(
|
||||||
|
"Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted")]
|
||||||
public bool UsePrivilegedIntents { get; set; }
|
public bool UsePrivilegedIntents { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
The number of shards that the bot will be running on.
|
The number of shards that the bot will be running on.
|
||||||
Leave at 1 if you don't know what you're doing.
|
Leave at 1 if you don't know what you're doing.
|
||||||
|
|
||||||
note: If you are planning to have more than one shard, then you must change botCache to 'redis'.
|
note: If you are planning to have more than one shard, then you must change botCache to 'redis'.
|
||||||
Also, in that case you should be using NadekoBot.Coordinator to start the bot, and it will correctly override this value.
|
Also, in that case you should be using NadekoBot.Coordinator to start the bot, and it will correctly override this value.
|
||||||
""")]
|
""")]
|
||||||
public int TotalShards { get; set; }
|
public int TotalShards { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
@@ -37,34 +38,34 @@ public sealed class Creds : IBotCredentials
|
|||||||
For example '@Bot how's the weather in Paris' will return the current weather in Paris as if you were to run `.weather Paris` command.
|
For example '@Bot how's the weather in Paris' will return the current weather in Paris as if you were to run `.weather Paris` command.
|
||||||
""")]
|
""")]
|
||||||
public string NadekoAiToken { get; set; }
|
public string NadekoAiToken { get; set; }
|
||||||
|
|
||||||
[Comment(
|
[Comment(
|
||||||
"""
|
"""
|
||||||
Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
|
Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
|
||||||
Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
|
Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
|
||||||
Used only for Youtube Data Api (at the moment).
|
Used only for Youtube Data Api (at the moment).
|
||||||
""")]
|
""")]
|
||||||
public string GoogleApiKey { get; set; }
|
public string GoogleApiKey { get; set; }
|
||||||
|
|
||||||
[Comment(
|
[Comment(
|
||||||
"""
|
"""
|
||||||
Create a new custom search here https://programmablesearchengine.google.com/cse/create/new
|
Create a new custom search here https://programmablesearchengine.google.com/cse/create/new
|
||||||
Enable SafeSearch
|
Enable SafeSearch
|
||||||
Remove all Sites to Search
|
Remove all Sites to Search
|
||||||
Enable Search the entire web
|
Enable Search the entire web
|
||||||
Copy the 'Search Engine ID' to the SearchId field
|
Copy the 'Search Engine ID' to the SearchId field
|
||||||
|
|
||||||
Do all steps again but enable image search for the ImageSearchId
|
Do all steps again but enable image search for the ImageSearchId
|
||||||
""")]
|
""")]
|
||||||
public GoogleApiConfig Google { get; set; }
|
public GoogleApiConfig Google { get; set; }
|
||||||
|
|
||||||
[Comment("""Settings for voting system for discordbots. Meant for use on global Nadeko.""")]
|
[Comment("""Settings for voting system for discordbots. Meant for use on global Nadeko.""")]
|
||||||
public VotesSettings Votes { get; set; }
|
public VotesSettings Votes { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Patreon auto reward system settings.
|
Patreon auto reward system settings.
|
||||||
go to https://www.patreon.com/portal -> my clients -> create client
|
go to https://www.patreon.com/portal -> my clients -> create client
|
||||||
""")]
|
""")]
|
||||||
public PatreonSettings Patreon { get; set; }
|
public PatreonSettings Patreon { get; set; }
|
||||||
|
|
||||||
[Comment("""Api key for sending stats to DiscordBotList.""")]
|
[Comment("""Api key for sending stats to DiscordBotList.""")]
|
||||||
@@ -75,27 +76,27 @@ public sealed class Creds : IBotCredentials
|
|||||||
|
|
||||||
[Comment(@"OpenAi api key.")]
|
[Comment(@"OpenAi api key.")]
|
||||||
public string Gpt3ApiKey { get; set; }
|
public string Gpt3ApiKey { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Which cache implementation should bot use.
|
Which cache implementation should bot use.
|
||||||
'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset.
|
'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset.
|
||||||
'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml
|
'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml
|
||||||
""")]
|
""")]
|
||||||
public BotCacheImplemenation BotCache { get; set; }
|
public BotCacheImplemenation BotCache { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Redis connection string. Don't change if you don't know what you're doing.
|
Redis connection string. Don't change if you don't know what you're doing.
|
||||||
Only used if botCache is set to 'redis'
|
Only used if botCache is set to 'redis'
|
||||||
""")]
|
""")]
|
||||||
public string RedisOptions { get; set; }
|
public string RedisOptions { get; set; }
|
||||||
|
|
||||||
[Comment("""Database options. Don't change if you don't know what you're doing. Leave null for default values""")]
|
[Comment("""Database options. Don't change if you don't know what you're doing. Leave null for default values""")]
|
||||||
public DbOptions Db { get; set; }
|
public DbOptions Db { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Address and port of the coordinator endpoint. Leave empty for default.
|
Address and port of the coordinator endpoint. Leave empty for default.
|
||||||
Change only if you've changed the coordinator address or port.
|
Change only if you've changed the coordinator address or port.
|
||||||
""")]
|
""")]
|
||||||
public string CoordinatorUrl { get; set; }
|
public string CoordinatorUrl { get; set; }
|
||||||
|
|
||||||
[Comment(
|
[Comment(
|
||||||
@@ -103,23 +104,23 @@ public sealed class Creds : IBotCredentials
|
|||||||
public string RapidApiKey { get; set; }
|
public string RapidApiKey { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
https://locationiq.com api key (register and you will receive the token in the email).
|
https://locationiq.com api key (register and you will receive the token in the email).
|
||||||
Used only for .time command.
|
Used only for .time command.
|
||||||
""")]
|
""")]
|
||||||
public string LocationIqApiKey { get; set; }
|
public string LocationIqApiKey { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
https://timezonedb.com api key (register and you will receive the token in the email).
|
https://timezonedb.com api key (register and you will receive the token in the email).
|
||||||
Used only for .time command
|
Used only for .time command
|
||||||
""")]
|
""")]
|
||||||
public string TimezoneDbApiKey { get; set; }
|
public string TimezoneDbApiKey { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use.
|
https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use.
|
||||||
Used for cryptocurrency related commands.
|
Used for cryptocurrency related commands.
|
||||||
""")]
|
""")]
|
||||||
public string CoinmarketcapApiKey { get; set; }
|
public string CoinmarketcapApiKey { get; set; }
|
||||||
|
|
||||||
// [Comment(@"https://polygon.io/dashboard/api-keys api key. Free plan allows for 5 queries per minute.
|
// [Comment(@"https://polygon.io/dashboard/api-keys api key. Free plan allows for 5 queries per minute.
|
||||||
// Used for stocks related commands.")]
|
// Used for stocks related commands.")]
|
||||||
// public string PolygonIoApiKey { get; set; }
|
// public string PolygonIoApiKey { get; set; }
|
||||||
@@ -128,9 +129,9 @@ public sealed class Creds : IBotCredentials
|
|||||||
public string OsuApiKey { get; set; }
|
public string OsuApiKey { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Optional Trovo client id.
|
Optional Trovo client id.
|
||||||
You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors.
|
You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors.
|
||||||
""")]
|
""")]
|
||||||
public string TrovoClientId { get; set; }
|
public string TrovoClientId { get; set; }
|
||||||
|
|
||||||
[Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")]
|
[Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")]
|
||||||
@@ -140,23 +141,35 @@ public sealed class Creds : IBotCredentials
|
|||||||
public string TwitchClientSecret { get; set; }
|
public string TwitchClientSecret { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Command and args which will be used to restart the bot.
|
Command and args which will be used to restart the bot.
|
||||||
Only used if bot is executed directly (NOT through the coordinator)
|
Only used if bot is executed directly (NOT through the coordinator)
|
||||||
placeholders:
|
placeholders:
|
||||||
{0} -> shard id
|
{0} -> shard id
|
||||||
{1} -> total shards
|
{1} -> total shards
|
||||||
Linux default
|
Linux default
|
||||||
cmd: dotnet
|
cmd: dotnet
|
||||||
args: "NadekoBot.dll -- {0}"
|
args: "NadekoBot.dll -- {0}"
|
||||||
Windows default
|
Windows default
|
||||||
cmd: NadekoBot.exe
|
cmd: NadekoBot.exe
|
||||||
args: "{0}"
|
args: "{0}"
|
||||||
""")]
|
""")]
|
||||||
public RestartConfig RestartCommand { get; set; }
|
public RestartConfig RestartCommand { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
[Comment("""
|
||||||
|
Settings for the grpc api.
|
||||||
|
We don't provide support for this.
|
||||||
|
If you leave certPath empty, the api will run on http.
|
||||||
|
""")]
|
||||||
|
public GrpcApiConfig GrpcApi { get; set; }
|
||||||
|
|
||||||
|
[Comment("""
|
||||||
|
Url and api key to a seq server. If url is set, bot will try to send logs to it.
|
||||||
|
""")]
|
||||||
|
public SeqConfig Seq { get; set; }
|
||||||
|
|
||||||
public Creds()
|
public Creds()
|
||||||
{
|
{
|
||||||
Version = 9;
|
|
||||||
Token = string.Empty;
|
Token = string.Empty;
|
||||||
UsePrivilegedIntents = true;
|
UsePrivilegedIntents = true;
|
||||||
OwnerIds = new List<ulong>();
|
OwnerIds = new List<ulong>();
|
||||||
@@ -179,24 +192,27 @@ public sealed class Creds : IBotCredentials
|
|||||||
|
|
||||||
RestartCommand = new RestartConfig();
|
RestartCommand = new RestartConfig();
|
||||||
Google = new GoogleApiConfig();
|
Google = new GoogleApiConfig();
|
||||||
|
|
||||||
|
GrpcApi = new();
|
||||||
|
Seq = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DbOptions
|
public class DbOptions
|
||||||
: IDbOptions
|
: IDbOptions
|
||||||
{
|
{
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Database type. "sqlite", "mysql" and "postgresql" are supported.
|
Database type. "sqlite", "mysql" and "postgresql" are supported.
|
||||||
Default is "sqlite"
|
Default is "sqlite"
|
||||||
""")]
|
""")]
|
||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Database connection string.
|
Database connection string.
|
||||||
You MUST change this if you're not using "sqlite" type.
|
You MUST change this if you're not using "sqlite" type.
|
||||||
Default is "Data Source=data/NadekoBot.db"
|
Default is "Data Source=data/NadekoBot.db"
|
||||||
Example for mysql: "Server=localhost;Port=3306;Uid=root;Pwd=my_super_secret_mysql_password;Database=nadeko"
|
Example for mysql: "Server=localhost;Port=3306;Uid=root;Pwd=my_super_secret_mysql_password;Database=nadeko"
|
||||||
Example for postgresql: "Server=localhost;Port=5432;User Id=postgres;Password=my_super_secret_postgres_password;Database=nadeko;"
|
Example for postgresql: "Server=localhost;Port=5432;User Id=postgres;Password=my_super_secret_postgres_password;Database=nadeko;"
|
||||||
""")]
|
""")]
|
||||||
public string ConnectionString { get; set; }
|
public string ConnectionString { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,29 +247,29 @@ public sealed class Creds : IBotCredentials
|
|||||||
public sealed record VotesSettings : IVotesSettings
|
public sealed record VotesSettings : IVotesSettings
|
||||||
{
|
{
|
||||||
[Comment("""
|
[Comment("""
|
||||||
top.gg votes service url
|
top.gg votes service url
|
||||||
This is the url of your instance of the NadekoBot.Votes api
|
This is the url of your instance of the NadekoBot.Votes api
|
||||||
Example: https://votes.my.cool.bot.com
|
Example: https://votes.my.cool.bot.com
|
||||||
""")]
|
""")]
|
||||||
public string TopggServiceUrl { get; set; }
|
public string TopggServiceUrl { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Authorization header value sent to the TopGG service url with each request
|
Authorization header value sent to the TopGG service url with each request
|
||||||
This should be equivalent to the TopggKey in your NadekoBot.Votes api appsettings.json file
|
This should be equivalent to the TopggKey in your NadekoBot.Votes api appsettings.json file
|
||||||
""")]
|
""")]
|
||||||
public string TopggKey { get; set; }
|
public string TopggKey { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
discords.com votes service url
|
discords.com votes service url
|
||||||
This is the url of your instance of the NadekoBot.Votes api
|
This is the url of your instance of the NadekoBot.Votes api
|
||||||
Example: https://votes.my.cool.bot.com
|
Example: https://votes.my.cool.bot.com
|
||||||
""")]
|
""")]
|
||||||
public string DiscordsServiceUrl { get; set; }
|
public string DiscordsServiceUrl { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Authorization header value sent to the Discords service url with each request
|
Authorization header value sent to the Discords service url with each request
|
||||||
This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsettings.json file
|
This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsettings.json file
|
||||||
""")]
|
""")]
|
||||||
public string DiscordsKey { get; set; }
|
public string DiscordsKey { get; set; }
|
||||||
|
|
||||||
public VotesSettings()
|
public VotesSettings()
|
||||||
@@ -272,13 +288,25 @@ public sealed class Creds : IBotCredentials
|
|||||||
DiscordsKey = discordsKey;
|
DiscordsKey = discordsKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed record GrpcApiConfig
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; } = false;
|
||||||
|
public string CertChain { get; set; } = string.Empty;
|
||||||
|
public string CertPrivateKey { get; set; } = string.Empty;
|
||||||
|
public string Host { get; set; } = "localhost";
|
||||||
|
public int Port { get; set; } = 43120;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SeqConfig
|
||||||
|
{
|
||||||
|
public string Url { get; init; }
|
||||||
|
public string ApiKey { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GoogleApiConfig : IGoogleApiConfig
|
public class GoogleApiConfig : IGoogleApiConfig
|
||||||
{
|
{
|
||||||
public string SearchId { get; init; }
|
public string SearchId { get; init; }
|
||||||
public string ImageSearchId { get; init; }
|
public string ImageSearchId { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -40,4 +40,11 @@ public interface ICurrencyService
|
|||||||
TxData? txData);
|
TxData? txData);
|
||||||
|
|
||||||
Task<IReadOnlyList<DiscordUser>> GetTopRichest(ulong ignoreId, int page = 0, int perPage = 9);
|
Task<IReadOnlyList<DiscordUser>> GetTopRichest(ulong ignoreId, int page = 0, int perPage = 9);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<CurrencyTransaction>> GetTransactionsAsync(
|
||||||
|
ulong userId,
|
||||||
|
int page,
|
||||||
|
int perPage = 15);
|
||||||
|
|
||||||
|
Task<int> GetTransactionsCountAsync(ulong userId);
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user