Compare commits

...

48 Commits

Author SHA1 Message Date
Kwoth
32fc8b6e03 docs: Upped version to 5.1.18, updated changelog 2024-11-03 03:44:33 +00:00
Kwoth
297e2fde0e change: timely 'password' is now a button 2024-11-03 03:41:34 +00:00
Kwoth
729f26caab button for timely 2024-11-03 02:48:39 +00:00
Kwoth
4b12e4e923 dev: Removed discrim from the database
add: .translateflags command
add: captcha to timely, configurable in .conf gambling
change: change bonuses for patreon rewards
fix: nunchi message color fix
2024-11-02 16:23:58 +00:00
Kwoth
12f4ce7f2a change: animal race will update more frequently, but animals will move slightly slower. Overall everything will be slightly faster 2024-11-01 04:53:20 +00:00
Kwoth
00944e08c3 fix: .ncs will now show an error if setting a pixel fails 2024-11-01 04:52:29 +00:00
Kwoth
569abd7194 api: work on server xp api 2024-10-31 11:48:31 +00:00
Kwoth
474a1db41d add: timely now has a 3 letter password by default. Configurable via .conf gamb 2024-10-31 11:48:09 +00:00
Kwoth
0f6255947e fix: fixed ubl pagination 2024-10-30 13:16:04 +00:00
Kwoth
f68f219a25 fix: ytdataapiv3 searches will no longer duplicate youtube urls 2024-10-30 07:02:13 +00:00
Kwoth
8f16b11d02 api: finance api implementation 2024-10-29 08:15:53 +00:00
Kwoth
df5eced904 change: Error sending greet dm will now be a warning
change: initial canvas price down to 3 from 10, 10 is way too expensive
2024-10-29 01:53:42 +00:00
Kwoth
1dcd158f43 fix: Bot will now not accept .aar Role if that Role is higher than or equal to bot's role. Previously bot would just fail silently, now there is a proper error message. 2024-10-28 21:42:05 +00:00
Kwoth
757c9b564d Updated changelog. Version upped to 5.1.16 2024-10-28 08:21:27 +00:00
Kwoth
07cef3eb5e Added .nc 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.
see whole canvas: .nc
set pixel: .ncsp <pos> <color> <text?>
get pixel: .ncp <pos>
zoom: .ncz <pos> or .ncz x y
2024-10-28 08:17:23 +00:00
Kwoth
85c525e19b api: added command feed and shard update feed 2024-10-23 21:29:40 +00:00
Kwoth
477581f616 docs: Updated CHANGELOG, upped version to 5.1.15 2024-10-21 14:29:08 +00:00
Kwoth
ff30105816 fix: Fixed several features which weren't getting loaded on startup 2024-10-21 14:15:42 +00:00
Kwoth
49f04a594b fix: fixed expire settings not returned on api 2024-10-21 04:36:54 +00:00
Kwoth
716090a132 fix: fix migration incase there is invalid data 2024-10-21 04:08:33 +00:00
Kwoth
c835514c7b dev: split warn punishments into a separate table
api: Added warn endpoints
fix: Reminders should now be able to ping everyone if the user who created the reminder has that permission
2024-10-21 03:14:46 +00:00
Kwoth
b136e7ff0e fix: author name will be counted as content in embeds. Embeds will now be valid if they only have an author specified 2024-10-19 19:21:00 +00:00
Kwoth
9dd2997b0f dev: added botonguild api endpoint 2024-10-16 15:15:41 +00:00
Kwoth
fde5309ea4 dev: added quote api 2024-10-16 01:52:56 +00:00
Kwoth
a8e4173e9b fix: grpc api fix 2024-10-13 21:37:58 +00:00
Kwoth
74b4c4b64d change: cleanup command will now also clear greetsettings and autpublish channels
dev: Cleaned up some comments, changed grpc api
2024-10-10 16:01:49 +00:00
Kwoth
6cc5a160a2 fix: .greetmsg (and related commands) and .greettest (and other greet test commands) will now show the correct response string when the toggle is disabled 2024-10-07 20:02:43 +00:00
Kwoth
ca8e022db6 fix: .waifulb will no longer show #0000 discriminators, for real this time 2024-10-07 12:56:28 +00:00
Kwoth
cd8c14c607 change: Leaderboards will show 10 users per page 2024-10-07 09:07:32 +00:00
Kwoth
1340533c21 fix: fix cleanup migration 2024-10-06 14:17:26 +00:00
Kwoth
14d86b9042 fix: grpc api fix 2024-10-04 03:31:44 +00:00
Kwoth
3a504a954f add: Added options '-c' option for '.xpglb' which will show global xp leaderboard only with this server's users 2024-10-04 03:24:18 +00:00
Kwoth
822ce0b8de fix: Alias collision fixed, .qse will be quotesearch, .qs will remain queuesearch (music)
fix: Improved guild config cleanup migration by removing invalid Permissiosnv2 entries (thx Leon)
2024-10-04 02:00:46 +00:00
Kwoth
40490a4656 docs: Version upped to 5.1.14, updated CHANGELOG.md 2024-10-03 13:01:11 +00:00
Kwoth
0cf7909fef change: improved .xplb -c, it will now correctly work only on users who are still in the server, isntead of only top 1k
fix: Fixed medusa error on bot startup
2024-10-03 12:58:45 +00:00
Kwoth
de8d4b7d9e docs: Version upped to 5.1.13, updated CHANGELOG.md
fix: Fixed seq comment in creds
2024-10-03 12:05:16 +00:00
Kwoth
0123892038 fix: Grpc api will no longer start unless it's enabled in creds 2024-10-03 11:59:47 +00:00
Kwoth
d00e59567a fix: Fixed greet/bye messages showing wrong message in the wrong server sometimes
docs: Version upped to 5.1.12. Updated CHANGELOG.md
2024-10-03 11:31:28 +00:00
Kwoth
0aba2fdcaf fix: expressions will no longer cause exceptions if the bot doesn't have perms to write in the target channel
dev: Cleaned up expr code a little bit
2024-10-03 03:07:57 +00:00
Kwoth
bb910a8188 docs: updated CHANGELOG.md
docs: Version upped to 5.1.11
2024-10-03 02:42:36 +00:00
Kwoth
bdad9cc17a dev: fixed build warnings 2024-10-03 02:05:55 +00:00
Kwoth
5d76a15dc0 add: Added grpc api, perm system
add: grpc api config in creds
2024-10-03 02:01:03 +00:00
Kwoth
a7be56a562 fix: Possible fixes for buggy .bye behavior, ref #437 2024-10-03 01:58:18 +00:00
Kwoth
3c108e531e dev: Added initial version of the grpc api. Added relevant dummy settings to creds (they have no effect rn)
dev: Yt searches now INTERNALLY return multiple results but there is no way right now to paginate plain text results
dev: moved some stuff around
2024-09-26 07:26:18 +00:00
Kwoth
c473669cbc docs: Version upped to 5.1.10, updated changelog 2024-09-24 02:00:02 +00:00
Kwoth
b97c486b80 dev: added some logs in greet service 2024-09-24 01:57:19 +00:00
Kwoth
716e092fd0 fix: Fixed claimed waifu decay that was introduced in a recent patch
dev: Cleaned up a little bit in medusa loading. Clean medusa unloading will be broken for a while probably
2024-09-23 18:18:38 +00:00
Kwoth
a362ee90fc dev: forgot to update the version in csproj, again 2024-09-22 01:51:42 +00:00
157 changed files with 26009 additions and 1269 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nadeko.Medusa", "src\Nadeko
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Generators", "src\NadekoBot.Generators\NadekoBot.Generators.csproj", "{92770AF3-83EE-49F1-A0BB-79124D19A13D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.GrpcApiBase", "src\NadekoBot.GrpcApiBase\NadekoBot.GrpcApiBase.csproj", "{FB74B9EA-10B9-4542-ACB1-35523A95A587}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -91,6 +99,7 @@ Global
{E685977E-31A4-46F4-A5D7-4E3E39E82E43} = {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}
{FB74B9EA-10B9-4542-ACB1-35523A95A587} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F3F555C-855F-4BE8-B526-D062D3E8ACA4}

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

View File

@@ -9,7 +9,7 @@
</PropertyGroup>
<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="Newtonsoft.Json" Version="13.0.3" PrivateAssets="all" GeneratePathProperty="true" />
</ItemGroup>

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

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

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

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

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

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

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

View File

@@ -0,0 +1,108 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
package xp;
service GrpcXp {
rpc GetXpLb(GetXpLbRequest) returns (GetXpLbReply);
rpc ResetUserXp(ResetUserXpRequest) returns (ResetUserXpReply);
rpc GetXpSettings(GetXpSettingsRequest) returns (GetXpSettingsReply);
rpc AddExclusion(AddExclusionRequest) returns (AddExclusionReply);
rpc DeleteExclusion(DeleteExclusionRequest) returns (DeleteExclusionReply);
rpc AddReward(AddRewardRequest) returns (AddRewardReply);
rpc DeleteReward(DeleteRewardRequest) returns (DeleteRewardReply);
}
message GetXpLbRequest {
uint64 guildId = 1;
int32 page = 2;
}
message GetXpLbReply {
repeated XpLbUserReply users = 1;
int32 total = 2;
}
message XpLbUserReply {
uint64 userId = 1;
string username = 2;
int64 xp = 3;
int64 level = 4;
string avatar = 5;
}
message ResetUserXpRequest {
uint64 guildId = 1;
uint64 userId = 2;
}
message ResetUserXpReply {
bool success = 1;
}
message GetXpSettingsReply {
repeated ExclItemReply exclusions = 1;
repeated RewItemReply rewards = 2;
bool serverExcluded = 3;
}
message GetXpSettingsRequest {
uint64 guildId = 1;
}
message ExclItemReply {
string type = 1;
uint64 id = 2;
string name = 3;
}
message RewItemReply {
int32 level = 1;
string type = 2;
string value = 3;
}
message AddExclusionRequest {
uint64 guildId = 1;
string type = 2;
uint64 id = 3;
}
message AddExclusionReply {
bool success = 1;
}
message DeleteExclusionRequest {
uint64 guildId = 1;
string type = 2;
uint64 id = 3;
}
message DeleteExclusionReply {
bool success = 1;
}
message AddRewardRequest {
uint64 guildId = 1;
int32 level = 2;
string type = 3;
string value = 4;
}
message AddRewardReply {
bool success = 1;
}
message DeleteRewardRequest {
uint64 guildId = 1;
int32 level = 2;
string type = 3;
}
message DeleteRewardReply {
bool success = 1;
}

View File

@@ -25,7 +25,7 @@ public sealed class Bot : IBot
public bool IsReady { get; private set; }
public int ShardId { get; set; }
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private readonly CommandService _commandService;
private readonly DbService _db;
@@ -42,6 +42,9 @@ public sealed class Bot : IBot
_credsProvider = new BotCredsProvider(totalShards, credPath);
_creds = _credsProvider.GetCreds();
LogSetup.SetupLogger(shardId, _creds);
Log.Information("Pid: {ProcessId}", Environment.ProcessId);
_db = new NadekoDbService(_credsProvider);
var messageCacheSize =
@@ -115,7 +118,7 @@ public sealed class Bot : IBot
// svcs.Components.Remove<IPlanner, Planner>();
// svcs.Components.Add<IPlanner, RemovablePlanner>();
svcs.AddSingleton<IBotCredentials>(_ => _credsProvider.GetCreds());
svcs.AddSingleton<IBotCreds>(_ => _credsProvider.GetCreds());
svcs.AddSingleton<DbService, DbService>(_db);
svcs.AddSingleton<IBotCredsProvider>(_credsProvider);
svcs.AddSingleton<DiscordSocketClient>(Client);

View File

@@ -25,7 +25,6 @@ public static class DiscordUserExtensions
{
UserId = userId,
Username = username,
Discriminator = discrim,
AvatarId = avatarId,
TotalXp = 0,
CurrencyAmount = 0
@@ -33,7 +32,6 @@ public static class DiscordUserExtensions
old => new()
{
Username = username,
Discriminator = discrim,
AvatarId = avatarId
},
() => new()
@@ -49,8 +47,7 @@ public static class DiscordUserExtensions
() => new()
{
UserId = userId,
Username = "Unknown",
Discriminator = "????",
Username = "??Unknown",
AvatarId = string.Empty,
TotalXp = 0,
CurrencyAmount = 0
@@ -87,14 +84,7 @@ public static class DiscordUserExtensions
> users.AsQueryable().Where(y => y.UserId == id).Select(y => y.TotalXp).FirstOrDefault())
.Count()
+ 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(
this DbSet<DiscordUser> users,
ulong botId,

View File

@@ -57,8 +57,7 @@ public static class GuildConfigExtensions
List<ulong> availableGuilds)
{
var result = await configs
.AsQueryable()
.Include(x => x.CommandCooldowns)
.IncludeEverything()
.Where(x => availableGuilds.Contains(x.GuildId))
.AsNoTracking()
.ToArrayAsync();
@@ -96,7 +95,6 @@ public static class GuildConfigExtensions
GuildId = guildId,
Permissions = Permissionv2.GetDefaultPermlist,
WarningsInitialized = true,
WarnPunishments = DefaultWarnPunishments
});
ctx.SaveChanges();
}
@@ -104,7 +102,6 @@ public static class GuildConfigExtensions
if (!config.WarningsInitialized)
{
config.WarningsInitialized = true;
config.WarnPunishments = DefaultWarnPunishments;
}
return config;

View File

@@ -26,17 +26,6 @@ public static class UserXpExtensions
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)
=> await xps.ToLinqToDBTable()
.Where(x => x.GuildId == guildId)
@@ -55,9 +44,6 @@ public static class UserXpExtensions
.CountAsyncLinqToDB()
+ 1;
public static void ResetGuildUserXp(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
=> xps.Delete(x => x.UserId == userId && x.GuildId == guildId);
public static void ResetGuildXp(this DbSet<UserXpStats> xps, ulong guildId)
=> xps.Delete(x => x.GuildId == guildId);

View File

@@ -7,7 +7,7 @@ public class DiscordUser : DbEntity
{
public ulong UserId { get; set; }
public string Username { get; set; }
public string Discriminator { get; set; }
// public string Discriminator { get; set; }
public string AvatarId { get; set; }
public int? ClubId { get; set; }
@@ -27,9 +27,6 @@ public class DiscordUser : DbEntity
public override string ToString()
{
if (string.IsNullOrWhiteSpace(Discriminator) || Discriminator == "0000")
return Username;
return Username + "#" + Discriminator;
return Username;
}
}

View File

@@ -0,0 +1,8 @@
#nullable disable
namespace NadekoBot.Db.Models;
public class FlagTranslateChannel : DbEntity
{
public ulong GuildId { get; set; }
public ulong ChannelId { get; set; }
}

View File

@@ -77,7 +77,6 @@ public class GuildConfig : DbEntity
public HashSet<UnroleTimer> UnroleTimer { get; set; } = new();
public HashSet<VcRoleInfo> VcRoleInfos { get; set; }
public HashSet<CommandAlias> CommandAliases { get; set; } = new();
public List<WarningPunishment> WarnPunishments { get; set; } = new();
public bool WarningsInitialized { get; set; }
public HashSet<SlowmodeIgnoredUser> SlowmodeIgnoredUsers { get; set; }
public HashSet<SlowmodeIgnoredRole> SlowmodeIgnoredRoles { get; set; }

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

View File

@@ -3,6 +3,7 @@ namespace NadekoBot.Db.Models;
public class WarningPunishment : DbEntity
{
public ulong GuildId { get; set; }
public int Count { get; set; }
public PunishmentAction Punishment { get; set; }
public int Time { get; set; }

View File

@@ -62,7 +62,6 @@ public abstract class NadekoContext : DbContext
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
public DbSet<HoneypotChannel> HoneyPotChannels { get; set; }
// todo add guild colors
// public DbSet<GuildColors> GuildColors { get; set; }
@@ -74,6 +73,24 @@ public abstract class NadekoContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region Flag Translate
modelBuilder.Entity<FlagTranslateChannel>()
.HasIndex(x => new { x.GuildId, x.ChannelId })
.IsUnique();
#endregion
#region NCanvas
modelBuilder.Entity<NCPixel>()
.HasAlternateKey(x => x.Position);
modelBuilder.Entity<NCPixel>()
.HasIndex(x => x.OwnerId);
#endregion
#region QUOTES
var quoteEntity = modelBuilder.Entity<Quote>();
@@ -195,11 +212,6 @@ public abstract class NadekoContext : DbContext
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<GuildConfig>()
.HasMany(x => x.WarnPunishments)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<GuildConfig>()
.HasMany(x => x.SlowmodeIgnoredRoles)
.WithOne()
@@ -277,6 +289,18 @@ public abstract class NadekoContext : DbContext
#endregion
#region WarningPunishments
var warnpunishmentEntity = modelBuilder.Entity<WarningPunishment>(b =>
{
b.HasAlternateKey(x => new
{
x.GuildId,
x.Count
});
});
#endregion
#region Self Assignable Roles
@@ -339,6 +363,7 @@ public abstract class NadekoContext : DbContext
du.HasIndex(x => x.TotalXp);
du.HasIndex(x => x.CurrencyAmount);
du.HasIndex(x => x.UserId);
du.HasIndex(x => x.Username);
});
#endregion
@@ -695,7 +720,7 @@ public abstract class NadekoContext : DbContext
gs
.Property(x => x.IsEnabled)
.HasDefaultValue(false);
gs
.Property(x => x.AutoDeleteTimer)
.HasDefaultValue(0);

View File

@@ -5,6 +5,11 @@ namespace NadekoBot.Migrations;
public static class MigrationQueries
{
public static void UpdateUsernames(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("UPDATE DiscordUser SET Username = '??' + Username WHERE Discriminator = '????';");
}
public static void MigrateRero(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsSqlite())
@@ -38,6 +43,7 @@ left join guildconfigs on reactionrolemessage.guildconfigid = guildconfigs.id;")
DELETE FROM "DelMsgOnCmdChannel" WHERE "GuildConfigId" is NULL;
DELETE FROM "WarningPunishment" WHERE "GuildConfigId" NOT IN (SELECT "Id" from "GuildConfigs");
DELETE FROM "StreamRoleBlacklistedUser" WHERE "StreamRoleSettingsId" is NULL;
DELETE FROM "Permissions" WHERE "GuildConfigId" NOT IN (SELECT "Id" from "GuildConfigs");
""");
}
@@ -65,4 +71,20 @@ left join guildconfigs on reactionrolemessage.guildconfigid = guildconfigs.id;")
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
);
""");
}
}

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class nodiscrimandflagtranslate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
MigrationQueries.UpdateUsernames(migrationBuilder);
migrationBuilder.DropColumn(
name: "discriminator",
table: "discorduser");
migrationBuilder.CreateTable(
name: "flagtranslatechannel",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_flagtranslatechannel", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_flagtranslatechannel_guildid_channelid",
table: "flagtranslatechannel",
columns: new[] { "guildid", "channelid" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "flagtranslatechannel");
migrationBuilder.AddColumn<string>(
name: "discriminator",
table: "discorduser",
type: "text",
nullable: true);
}
}
}

View File

@@ -751,10 +751,6 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<string>("Discriminator")
.HasColumnType("text")
.HasColumnName("discriminator");
b.Property<bool>("IsClubAdmin")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
@@ -799,6 +795,9 @@ namespace NadekoBot.Migrations.PostgreSql
b.HasIndex("UserId")
.HasDatabaseName("ix_discorduser_userid");
b.HasIndex("Username")
.HasDatabaseName("ix_discorduser_username");
b.ToTable("discorduser", (string)null);
});
@@ -995,6 +994,37 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("filteredword", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.FlagTranslateChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("ChannelId")
.HasColumnType("numeric(20,0)")
.HasColumnName("channelid");
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.HasKey("Id")
.HasName("pk_flagtranslatechannel");
b.HasIndex("GuildId", "ChannelId")
.IsUnique()
.HasDatabaseName("ix_flagtranslatechannel_guildid_channelid");
b.ToTable("flagtranslatechannel", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b =>
{
b.Property<int>("Id")
@@ -1627,6 +1657,49 @@ namespace NadekoBot.Migrations.PostgreSql
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 =>
{
b.Property<int>("Id")
@@ -2938,9 +3011,9 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<int?>("GuildConfigId")
.HasColumnType("integer")
.HasColumnName("guildconfigid");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<int>("Punishment")
.HasColumnType("integer")
@@ -2957,8 +3030,8 @@ namespace NadekoBot.Migrations.PostgreSql
b.HasKey("Id")
.HasName("pk_warningpunishment");
b.HasIndex("GuildConfigId")
.HasDatabaseName("ix_warningpunishment_guildconfigid");
b.HasAlternateKey("GuildId", "Count")
.HasName("ak_warningpunishment_guildid_count");
b.ToTable("warningpunishment", (string)null);
});
@@ -3616,15 +3689,6 @@ namespace NadekoBot.Migrations.PostgreSql
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 =>
{
b.HasOne("NadekoBot.Db.Models.XpSettings", "XpSettings")
@@ -3740,8 +3804,6 @@ namespace NadekoBot.Migrations.PostgreSql
b.Navigation("VcRoleInfos");
b.Navigation("WarnPunishments");
b.Navigation("XpSettings");
});

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
/// <inheritdoc />
public partial class nodiscrimandflagtranslate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
MigrationQueries.UpdateUsernames(migrationBuilder);
migrationBuilder.DropColumn(
name: "Discriminator",
table: "DiscordUser");
migrationBuilder.CreateTable(
name: "FlagTranslateChannel",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_FlagTranslateChannel", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_FlagTranslateChannel_GuildId_ChannelId",
table: "FlagTranslateChannel",
columns: new[] { "GuildId", "ChannelId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "FlagTranslateChannel");
migrationBuilder.AddColumn<string>(
name: "Discriminator",
table: "DiscordUser",
type: "TEXT",
nullable: true);
}
}
}

View File

@@ -560,9 +560,6 @@ namespace NadekoBot.Migrations
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<string>("Discriminator")
.HasColumnType("TEXT");
b.Property<bool>("IsClubAdmin")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
@@ -596,6 +593,8 @@ namespace NadekoBot.Migrations
b.HasIndex("UserId");
b.HasIndex("Username");
b.ToTable("DiscordUser");
});
@@ -741,6 +740,29 @@ namespace NadekoBot.Migrations
b.ToTable("FilteredWord");
});
modelBuilder.Entity("NadekoBot.Db.Models.FlagTranslateChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GuildId", "ChannelId")
.IsUnique();
b.ToTable("FlagTranslateChannel");
});
modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b =>
{
b.Property<int>("Id")
@@ -1213,6 +1235,38 @@ namespace NadekoBot.Migrations
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 =>
{
b.Property<int>("Id")
@@ -2183,7 +2237,7 @@ namespace NadekoBot.Migrations
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<int?>("GuildConfigId")
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<int>("Punishment")
@@ -2197,7 +2251,7 @@ namespace NadekoBot.Migrations
b.HasKey("Id");
b.HasIndex("GuildConfigId");
b.HasAlternateKey("GuildId", "Count");
b.ToTable("WarningPunishment");
});
@@ -2760,14 +2814,6 @@ namespace NadekoBot.Migrations
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 =>
{
b.HasOne("NadekoBot.Db.Models.XpSettings", "XpSettings")
@@ -2880,8 +2926,6 @@ namespace NadekoBot.Migrations
b.Navigation("VcRoleInfos");
b.Navigation("WarnPunishments");
b.Navigation("XpSettings");
});

View File

@@ -24,6 +24,13 @@ public partial class Administration
await Response().Error(strs.hierarchy).SendAsync();
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);
if (roles.Count == 0)

View File

@@ -1,4 +1,3 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
@@ -11,53 +10,63 @@ public class AutoPublishService : IExecNoCommand, IReadyExecutor, INService
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IBotCredsProvider _creds;
private ConcurrentDictionary<ulong, ulong> _enabled;
private ConcurrentDictionary<ulong, ulong> _enabled = new();
public AutoPublishService(DbService db, DiscordSocketClient client, IBotCredsProvider creds)
{
_db = db;
_client = client;
_creds = creds;
}
public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
}
public async Task ExecOnNoCommandAsync(IGuild? guild, IUserMessage msg)
{
if (guild is null)
return;
if (msg.Channel.GetChannelType() != ChannelType.News)
return;
if (!_enabled.TryGetValue(guild.Id, out var cid) || cid != msg.Channel.Id)
return;
await msg.CrosspostAsync(new RequestOptions()
{
RetryMode = RetryMode.AlwaysFail
});
}
// todo GUILDS
public async Task OnReadyAsync()
{
var creds = _creds.GetCreds();
await using var ctx = _db.GetDbContext();
var items = await ctx.GetTable<AutoPublishChannel>()
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId, creds.TotalShards, _client.ShardId))
.ToListAsyncLinqToDB();
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId, creds.TotalShards, _client.ShardId))
.ToListAsyncLinqToDB();
_enabled = items
.ToDictionary(x => x.GuildId, x => x.ChannelId)
.ToConcurrent();
.ToDictionary(x => x.GuildId, x => x.ChannelId)
.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)
{
await using var ctx = _db.GetDbContext();
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)
{
@@ -66,22 +75,22 @@ public class AutoPublishService : IExecNoCommand, IReadyExecutor, INService
}
await ctx.GetTable<AutoPublishChannel>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
ChannelId = channelId,
DateAdded = DateTime.UtcNow,
},
old => new()
{
ChannelId = channelId,
DateAdded = DateTime.UtcNow,
},
() => new()
{
GuildId = guildId
});
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
ChannelId = channelId,
DateAdded = DateTime.UtcNow,
},
old => new()
{
ChannelId = channelId,
DateAdded = DateTime.UtcNow,
},
() => new()
{
GuildId = guildId
});
_enabled[guildId] = channelId;
return true;

View File

@@ -206,6 +206,18 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, INService
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.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()
{

View File

@@ -138,8 +138,7 @@ public partial class Administration
[OwnerOnly]
public Task DeleteXp()
=> ConfirmActionInternalAsync("Delete Xp", () => _xcs.DeleteXp());
[Cmd]
[OwnerOnly]
public Task DeleteWaifus()

View File

@@ -200,9 +200,7 @@ public partial class Administration
if (!isEnabled)
{
var cmdName = GetCmdName(type);
await Response().Pending(strs.boostmsg_enable($"`{prefix}{cmdName}`")).SendAsync();
await SendGreetEnableHint(type);
}
}
@@ -226,19 +224,24 @@ public partial class Administration
await _service.Test(ctx.Guild.Id, type, (ITextChannel)ctx.Channel, user);
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 str = type switch
{
GreetType.Greet => strs.boostmsg_enable(cmd),
GreetType.Bye => strs.greetmsg_enable(cmd),
GreetType.Boost => strs.byemsg_enable(cmd),
GreetType.Greet => strs.greetmsg_enable(cmd),
GreetType.Bye => strs.byemsg_enable(cmd),
GreetType.Boost => strs.boostmsg_enable(cmd),
GreetType.GreetDm => strs.greetdmmsg_enable(cmd),
_ => strs.error
};
if (conf?.IsEnabled is not true)
await Response().Pending(str).SendAsync();
await Response().Pending(str).SendAsync();
}
}
}

View File

@@ -75,16 +75,27 @@ public class GreetService : INService, IReadyExecutor
_client.GuildMemberUpdated += ClientOnGuildMemberUpdated;
var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));
while (await timer.WaitForNextTickAsync())
while (true)
{
var (conf, user, ch) = await _greetQueue.Reader.ReadAsync();
await GreetUsers(conf, ch, user);
try
{
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)
{
if (!_enabled[GreetType.Boost].Contains(newUser.Guild.Id))
return Task.CompletedTask;
// if user is a new booster
// or boosted again the same server
if ((optOldUser.Value is { PremiumSince: null } && newUser is { PremiumSince: not null })
@@ -126,21 +137,63 @@ public class GreetService : INService, IReadyExecutor
.DeleteAsync();
}
private Task OnUserLeft(SocketGuild guild, SocketUser user)
private Task OnUserJoined(IGuildUser user)
{
_ = 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
{
var conf = await GetGreetSettingsAsync(guild.Id, GreetType.Bye);
if (conf is null)
if (conf?.ChannelId is not { } cid)
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
{
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);
return;
}
@@ -155,10 +208,11 @@ public class GreetService : INService, IReadyExecutor
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)
=> await _cache.GetOrAddAsync<GreetSettings?>(_greetSettingsKey,
=> await _cache.GetOrAddAsync<GreetSettings?>(GreetSettingsKey(gid, type),
() => InternalGetGreetSettingsAsync(gid, type),
TimeSpan.FromSeconds(3));
@@ -207,9 +261,10 @@ public class GreetService : INService, IReadyExecutor
or DiscordErrorCode.UnknownChannel)
{
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);
await SetGreet(channel.GuildId, channel.Id, GreetType.Greet, false);
await SetGreet(channel.GuildId, channel.Id, conf.GreetType, false);
}
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)
{
try
@@ -290,8 +337,9 @@ public class GreetService : INService, IReadyExecutor
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;
}
@@ -305,36 +353,6 @@ public class GreetService : INService, IReadyExecutor
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)
=> greetType switch
@@ -354,8 +372,8 @@ public class GreetService : INService, IReadyExecutor
{
await using var uow = _db.GetDbContext();
var q = uow.GetTable<GreetSettings>();
if(value is null)
if (value is null)
value = !_enabled[greetType].Contains(guildId);
if (value is { } v)
@@ -477,7 +495,8 @@ public class GreetService : INService, IReadyExecutor
{
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)

View File

@@ -13,7 +13,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
private readonly object _cacheLock = new();
@@ -24,7 +24,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
DiscordSocketClient client,
IPatronageService ps,
DbService db,
IBotCredentials creds)
IBotCreds creds)
{
_db = db;
_client = client;

View File

@@ -9,13 +9,13 @@ namespace NadekoBot.Modules.Administration;
public sealed class StickyRolesService : INService, IReadyExecutor
{
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private readonly DbService _db;
private HashSet<ulong> _stickyRoles = new();
public StickyRolesService(
DiscordSocketClient client,
IBotCredentials creds,
IBotCreds creds,
DbService db)
{
_client = client;

View File

@@ -15,7 +15,7 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
private readonly IBotStrings _strings;
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private ImmutableDictionary<ulong, IDMChannel> ownerChannels =
new Dictionary<ulong, IDMChannel>().ToImmutableDictionary();
@@ -36,7 +36,7 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
CommandHandler cmdHandler,
DbService db,
IBotStrings strings,
IBotCredentials creds,
IBotCreds creds,
IHttpClientFactory factory,
BotConfigService bss,
IPubSub pubSub,
@@ -453,7 +453,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
{
x.UserId,
x.Username,
x.Discriminator
})
.Where(x => users.Select(y => y.Id).Contains(x.UserId))
.ToArrayAsyncEF();
@@ -465,12 +464,11 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
UserId = x.Id,
AvatarId = x.AvatarId,
Username = x.Username,
Discriminator = x.Discriminator
});
var added = (await ctx.BulkCopyAsync(usersToAdd)).RowsCopied;
var toUpdateUserIds = presentDbUsers
.Where(x => x.Username == "Unknown" && x.Discriminator == "????")
.Where(x => x.Username.StartsWith("??"))
.Select(x => x.UserId)
.ToArray();
@@ -481,7 +479,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
.UpdateAsync(x => new DiscordUser()
{
Username = user.Username,
Discriminator = user.Discriminator,
// .award tends to set AvatarId and DateAdded to NULL, so account for that.
AvatarId = user.AvatarId,

View File

@@ -66,10 +66,10 @@ public partial class Administration
{
await _sender.Response(user)
.Embed(_sender.CreateEmbed()
.WithErrorColor()
.WithDescription(GetText(strs.warned_on(ctx.Guild.ToString())))
.AddField(GetText(strs.moderator), ctx.User.ToString())
.AddField(GetText(strs.reason), reason ?? "-"))
.WithErrorColor()
.WithDescription(GetText(strs.warned_on(ctx.Guild.ToString())))
.AddField(GetText(strs.moderator), ctx.User.ToString())
.AddField(GetText(strs.reason), reason ?? "-"))
.SendAsync();
}
catch
@@ -85,8 +85,9 @@ public partial class Administration
catch (Exception ex)
{
Log.Warning(ex, "Exception occured while warning a user");
var errorEmbed = _sender.CreateEmbed().WithErrorColor()
.WithDescription(GetText(strs.cant_apply_punishment));
var errorEmbed = _sender.CreateEmbed()
.WithErrorColor()
.WithDescription(GetText(strs.cant_apply_punishment));
if (dmFailed)
errorEmbed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
@@ -117,7 +118,7 @@ public partial class Administration
[Priority(1)]
public async Task WarnExpire()
{
var expireDays = await _service.GetWarnExpire(ctx.Guild.Id);
var (expireDays, _) = await _service.GetWarnExpire(ctx.Guild.Id);
if (expireDays == 0)
await Response().Confirm(strs.warns_dont_expire).SendAsync();
@@ -266,9 +267,9 @@ public partial class Administration
});
return _sender.CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.warnings_list))
.WithDescription(string.Join("\n", ws));
.WithOkColor()
.WithTitle(GetText(strs.warnings_list))
.WithDescription(string.Join("\n", ws));
})
.SendAsync();
}
@@ -278,7 +279,7 @@ public partial class Administration
[UserPerm(GuildPerm.Administrator)]
public Task WarnDelete(IGuildUser user, int index)
=> WarnDelete(user.Id, index);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
@@ -286,15 +287,15 @@ public partial class Administration
{
if (--index < 0)
return;
var warn = await _service.WarnDelete(userId, index);
var warn = await _service.WarnDelete(ctx.Guild.Id, userId, index);
if (warn is null)
{
await Response().Error(strs.warning_not_found).SendAsync();
return;
}
await Response().Confirm(strs.warning_deleted(Format.Bold(index.ToString()))).SendAsync();
}
@@ -311,7 +312,7 @@ public partial class Administration
{
if (index < 0)
return;
var success = await _service.WarnClearAsync(ctx.Guild.Id, userId, index, ctx.User.ToString());
var userStr = Format.Bold((ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString());
if (index == 0)
@@ -344,7 +345,7 @@ public partial class Administration
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)
return;
@@ -380,7 +381,7 @@ public partial class Administration
if (punish is PunishmentAction.TimeOut && time is null)
return;
var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time);
var success = await _service.WarnPunish(ctx.Guild.Id, number, punish, time);
if (!success)
return;
@@ -407,7 +408,7 @@ public partial class Administration
[UserPerm(GuildPerm.BanMembers)]
public async Task WarnPunish(int number)
{
if (!_service.WarnPunishRemove(ctx.Guild.Id, number))
if (!await _service.WarnPunishRemove(ctx.Guild.Id, number))
return;
await Response().Confirm(strs.warn_punish_rem(Format.Bold(number.ToString()))).SendAsync();
@@ -417,7 +418,7 @@ public partial class Administration
[RequireContext(ContextType.Guild)]
public async Task WarnPunishList()
{
var ps = _service.WarnPunishList(ctx.Guild.Id);
var ps = await _service.WarnPunishList(ctx.Guild.Id);
string list;
if (ps.Any())
@@ -478,13 +479,13 @@ public partial class Administration
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
await _mute.TimedBan(ctx.Guild, userId, time.Time, (ctx.User + " | " + msg).TrimTo(512), banPrune);
var toSend = _sender.CreateEmbed()
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true)
.AddField("ID", userId.ToString(), true)
.AddField(GetText(strs.duration),
time.Time.ToPrettyStringHm(),
true);
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true)
.AddField("ID", userId.ToString(), true)
.AddField(GetText(strs.duration),
time.Time.ToPrettyStringHm(),
true);
if (dmFailed)
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;
await ctx.Guild.AddBanAsync(userId, banPrune, (ctx.User + " | " + msg).TrimTo(512));
await Response().Embed(_sender.CreateEmbed()
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField("ID", userId.ToString(), true))
.SendAsync();
await Response()
.Embed(_sender.CreateEmbed()
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField("ID", userId.ToString(), true))
.SendAsync();
}
else
await Ban(user, msg);
@@ -543,10 +545,10 @@ public partial class Administration
await ctx.Guild.AddBanAsync(user, banPrune, (ctx.User + " | " + msg).TrimTo(512));
var toSend = _sender.CreateEmbed()
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField(GetText(strs.username), user.ToString(), true)
.AddField("ID", user.Id.ToString(), true);
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField(GetText(strs.username), user.ToString(), true)
.AddField("ID", user.Id.ToString(), true);
if (dmFailed)
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
@@ -738,10 +740,10 @@ public partial class Administration
catch { await ctx.Guild.RemoveBanAsync(user); }
var toSend = _sender.CreateEmbed()
.WithOkColor()
.WithTitle("☣ " + GetText(strs.sb_user))
.AddField(GetText(strs.username), user.ToString(), true)
.AddField("ID", user.Id.ToString(), true);
.WithOkColor()
.WithTitle("☣ " + GetText(strs.sb_user))
.AddField(GetText(strs.username), user.ToString(), true)
.AddField("ID", user.Id.ToString(), true);
if (dmFailed)
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
@@ -793,10 +795,10 @@ public partial class Administration
await user.KickAsync((ctx.User + " | " + msg).TrimTo(512));
var toSend = _sender.CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.kicked_user))
.AddField(GetText(strs.username), user.ToString(), true)
.AddField("ID", user.Id.ToString(), true);
.WithOkColor()
.WithTitle(GetText(strs.kicked_user))
.AddField(GetText(strs.username), user.ToString(), true)
.AddField("ID", user.Id.ToString(), true);
if (dmFailed)
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));
await _sender.Response(user)
.Embed(_sender.CreateEmbed()
.WithPendingColor()
.WithDescription(dmMessage))
.WithPendingColor()
.WithDescription(dmMessage))
.SendAsync();
}
catch
@@ -838,10 +840,10 @@ public partial class Administration
await user.SetTimeOutAsync(time.Time);
var toSend = _sender.CreateEmbed()
.WithOkColor()
.WithTitle("⏳ " + GetText(strs.timedout_user))
.AddField(GetText(strs.username), user.ToString(), true)
.AddField("ID", user.Id.ToString(), true);
.WithOkColor()
.WithTitle("⏳ " + GetText(strs.timedout_user))
.AddField(GetText(strs.username), user.ToString(), true)
.AddField("ID", user.Id.ToString(), true);
if (dmFailed)
toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user));
@@ -899,9 +901,9 @@ public partial class Administration
missStr = "-";
var toSend = _sender.CreateEmbed()
.WithDescription(GetText(strs.mass_ban_in_progress(banning.Count)))
.AddField(GetText(strs.invalid(missing.Count)), missStr)
.WithPendingColor();
.WithDescription(GetText(strs.mass_ban_in_progress(banning.Count)))
.AddField(GetText(strs.invalid(missing.Count)), missStr)
.WithPendingColor();
var banningMessage = await Response().Embed(toSend).SendAsync();
@@ -919,11 +921,13 @@ public partial class Administration
}
await banningMessage.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
.WithDescription(
GetText(strs.mass_ban_completed(banning.Count())))
.AddField(GetText(strs.invalid(missing.Count)), missStr)
.WithOkColor()
.Build());
.WithDescription(
GetText(strs.mass_ban_completed(
banning.Count())))
.AddField(GetText(strs.invalid(missing.Count)),
missStr)
.WithOkColor()
.Build());
}
[Cmd]
@@ -945,10 +949,10 @@ public partial class Administration
//send a message but don't wait for it
var banningMessageTask = Response()
.Embed(_sender.CreateEmbed()
.WithDescription(
GetText(strs.mass_kill_in_progress(bans.Count())))
.AddField(GetText(strs.invalid(missing)), missStr)
.WithPendingColor())
.WithDescription(
GetText(strs.mass_kill_in_progress(bans.Count())))
.AddField(GetText(strs.invalid(missing)), missStr)
.WithPendingColor())
.SendAsync();
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
@@ -966,11 +970,11 @@ public partial class Administration
var banningMessage = await banningMessageTask;
await banningMessage.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
.WithDescription(
GetText(strs.mass_kill_completed(bans.Count())))
.AddField(GetText(strs.invalid(missing)), missStr)
.WithOkColor()
.Build());
.WithDescription(
GetText(strs.mass_kill_completed(bans.Count())))
.AddField(GetText(strs.invalid(missing)), missStr)
.WithOkColor()
.Build());
}
public class WarnExpireOptions : INadekoCommandOptions

View File

@@ -1,9 +1,7 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Common.TypeReaders.Models;
using NadekoBot.Modules.Permissions.Services;
using NadekoBot.Db.Models;
using Newtonsoft.Json;
@@ -83,17 +81,24 @@ public class UserPunishService : INService, IReadyExecutor
};
long previousCount;
List<WarningPunishment> ps;
var ps = await WarnPunishList(guildId);
await using (var uow = _db.GetDbContext())
{
ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
previousCount = uow.Set<Warning>()
.ForId(guildId, userId)
.Where(w => !w.Forgiven && w.UserId == userId)
previousCount = uow.GetTable<Warning>()
.Where(w => w.GuildId == guildId && w.UserId == userId && !w.Forgiven)
.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();
}
@@ -260,12 +265,12 @@ public class UserPunishService : INService, IReadyExecutor
.ToListAsyncLinqToDB();
var cleared = await uow.GetTable<Warning>()
.Where(x => toClear.Contains(x.Id))
.UpdateAsync(_ => new()
{
Forgiven = true,
ForgivenBy = "expiry"
});
.Where(x => toClear.Contains(x.Id))
.UpdateAsync(_ => new()
{
Forgiven = true,
ForgivenBy = "expiry"
});
var toDelete = await uow.GetTable<Warning>()
.Where(x => uow.GetTable<GuildConfig>()
@@ -282,8 +287,8 @@ public class UserPunishService : INService, IReadyExecutor
.ToListAsyncLinqToDB();
var deleted = await uow.GetTable<Warning>()
.Where(x => toDelete.Contains(x.Id))
.DeleteAsync();
.Where(x => toDelete.Contains(x.Id))
.DeleteAsync();
if (cleared > 0 || deleted > 0)
{
@@ -324,11 +329,11 @@ public class UserPunishService : INService, IReadyExecutor
await uow.SaveChangesAsync();
}
public Task<int> GetWarnExpire(ulong guildId)
public Task<(int, bool)> GetWarnExpire(ulong guildId)
{
using var uow = _db.GetDbContext();
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)
@@ -377,18 +382,19 @@ public class UserPunishService : INService, IReadyExecutor
return toReturn;
}
public bool WarnPunish(
public async Task<bool> WarnPunish(
ulong guildId,
int number,
PunishmentAction punish,
StoopidTime time,
TimeSpan? time,
IRole role = null)
{
// these 3 don't make sense with time
if (punish is PunishmentAction.Softban or PunishmentAction.Kick or PunishmentAction.RemoveRoles
&& time is not null)
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;
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)
return false;
using var uow = _db.GetDbContext();
var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
var toDelete = ps.Where(x => x.Count == number);
uow.RemoveRange(toDelete);
ps.Add(new()
{
Count = number,
Punishment = punish,
Time = (int?)time?.Time.TotalMinutes ?? 0,
RoleId = punish == PunishmentAction.AddRole ? role!.Id : default(ulong?)
});
uow.SaveChanges();
var timeMinutes = (int?)time?.TotalMinutes ?? 0;
var roleId = punish == PunishmentAction.AddRole ? role!.Id : default(ulong?);
await using var uow = _db.GetDbContext();
await uow.GetTable<WarningPunishment>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
Count = number,
Punishment = punish,
Time = timeMinutes,
RoleId = roleId
},
_ => new()
{
Punishment = punish,
Time = timeMinutes,
RoleId = roleId
},
() => new()
{
GuildId = guildId,
Count = number
});
return true;
}
public bool WarnPunishRemove(ulong guildId, int number)
public async Task<bool> WarnPunishRemove(ulong guildId, int count)
{
if (number <= 0)
return false;
await using var uow = _db.GetDbContext();
var numDeleted = await uow.GetTable<WarningPunishment>()
.DeleteAsync(x => x.GuildId == guildId && x.Count == count);
using var uow = _db.GetDbContext();
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;
return numDeleted > 0;
}
public WarningPunishment[] WarnPunishList(ulong guildId)
public async Task<WarningPunishment[]> WarnPunishList(ulong guildId)
{
using var uow = _db.GetDbContext();
return uow.GuildConfigsForId(guildId, gc => gc.Include(x => x.WarnPunishments))
.WarnPunishments.OrderBy(x => x.Count)
.ToArray();
await using var uow = _db.GetDbContext();
var wps = uow.GetTable<WarningPunishment>()
.Where(x => x.GuildId == guildId)
.OrderBy(x => x.Count)
.ToArray();
return wps;
}
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);
}
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();
var warn = await uow.GetTable<Warning>()
.Where(x => x.UserId == userId)
.Where(x => x.GuildId == guildId && x.UserId == userId)
.OrderByDescending(x => x.DateAdded)
.Skip(index)
.FirstOrDefaultAsyncLinqToDB();
@@ -626,4 +636,73 @@ public class UserPunishService : INService, IReadyExecutor
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);
}
}

View File

@@ -6,48 +6,7 @@ namespace NadekoBot.Modules.NadekoExpressions;
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)]
public static WordPosition GetWordPosition(this ReadOnlySpan<char> str, in ReadOnlySpan<char> word)

View File

@@ -12,10 +12,10 @@ public partial class NadekoExpressions : NadekoModule<NadekoExpressionsService>
All
}
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private readonly IHttpClientFactory _clientFactory;
public NadekoExpressions(IBotCredentials creds, IHttpClientFactory clientFactory)
public NadekoExpressions(IBotCreds creds, IHttpClientFactory clientFactory)
{
_creds = creds;
_clientFactory = clientFactory;

View File

@@ -67,7 +67,6 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
// private readonly GlobalPermissionService _gperm;
// private readonly CmdCdService _cmdCds;
private readonly IPermissionChecker _permChecker;
private readonly ICommandHandler _cmd;
private readonly IBotStrings _strings;
private readonly IBot _bot;
private readonly IPubSub _pubSub;
@@ -84,7 +83,6 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
IBotStrings strings,
IBot bot,
DiscordSocketClient client,
ICommandHandler cmd,
IPubSub pubSub,
IMessageSenderService sender,
IReplacementService repSvc,
@@ -93,7 +91,6 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
{
_db = db;
_client = client;
_cmd = cmd;
_strings = strings;
_bot = bot;
_pubSub = pubSub;
@@ -249,46 +246,54 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
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(
guild,
msg.Channel,
msg.Author,
"ACTUALEXPRESSIONS",
expr.Trigger
);
if (!result.IsAllowed)
var cache = _pc.GetCacheFor(guild.Id);
if (cache.Verbose)
{
var cache = _pc.GetCacheFor(guild.Id);
if (cache.Verbose)
if (result.TryPickT3(out var disallowed, out _))
{
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();
foreach (var reaction in reactions)
@@ -336,6 +341,47 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
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)
{
NadekoExpression expr;
@@ -789,7 +835,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
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)
.Take(9)
.ToArray(), exprs.Length);

View File

@@ -116,7 +116,7 @@ public sealed class AnimalRace : IDisposable
{
foreach (var user in _users)
{
user.Progress += rng.Next(1, 11);
user.Progress += rng.Next(1, 10);
if (user.Progress >= 60)
user.Progress = 60;
}
@@ -126,7 +126,7 @@ public sealed class AnimalRace : IDisposable
FinishedUsers.AddRange(finished);
_ = OnStateUpdate?.Invoke(this);
await Task.Delay(2500);
await Task.Delay(1750);
}
if (FinishedUsers[0].Bet > 0)

View File

@@ -1,6 +1,7 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using LinqToDB.Tools;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Gambling.Bank;
using NadekoBot.Modules.Gambling.Common;
@@ -25,6 +26,7 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly NumberFormatInfo _enUsCulture;
private readonly DownloadTracker _tracker;
private readonly GamblingConfigService _configService;
private readonly FontProvider _fonts;
private readonly IBankService _bank;
private readonly IRemindService _remind;
private readonly GamblingTxTracker _gamblingTxTracker;
@@ -37,6 +39,7 @@ public partial class Gambling : GamblingModule<GamblingService>
DiscordSocketClient client,
DownloadTracker tracker,
GamblingConfigService configService,
FontProvider fonts,
IBankService bank,
IRemindService remind,
IPatronageService patronage,
@@ -51,12 +54,14 @@ public partial class Gambling : GamblingModule<GamblingService>
_remind = remind;
_gamblingTxTracker = gamblingTxTracker;
_ps = patronage;
_rng = new NadekoRandom();
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
_enUsCulture.NumberDecimalDigits = 0;
_enUsCulture.NumberGroupSeparator = "";
_tracker = tracker;
_configService = configService;
_fonts = fonts;
}
public async Task<string> GetBalanceStringAsync(ulong userId)
@@ -127,7 +132,7 @@ public partial class Gambling : GamblingModule<GamblingService>
customId: "timely:remind_me"),
(smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromHours(period)))
);
// Creates timely reminder button, parameter in milliseconds.
private NadekoInteractionBase CreateRemindMeInteraction(double ms)
=> _inter
@@ -139,6 +144,18 @@ public partial class Gambling : GamblingModule<GamblingService>
(smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromMilliseconds(ms)))
);
private NadekoInteractionBase CreateTimelyInteraction()
=> _inter
.Create(ctx.User.Id,
new ButtonBuilder(
label: "Timely",
emote: Emoji.Parse("💰"),
customId: "timely:" + _rng.Next(123456, 999999)),
async (smc) =>
{
await ClaimTimely();
});
[Cmd]
public async Task Timely()
{
@@ -150,6 +167,69 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
if (Config.Timely.RequirePassword)
{
// var password = _service.GeneratePassword();
//
// var img = new Image<Rgba32>(100, 40);
//
// var font = _fonts.NotoSans.CreateFont(30);
// var outlinePen = new SolidPen(Color.Black, 1f);
// var strikeoutRun = new RichTextRun
// {
// Start = 0,
// End = password.GetGraphemeCount(),
// Font = font,
// StrikeoutPen = new SolidPen(Color.White, 3),
// TextDecorations = TextDecorations.Strikeout
// };
// // draw password on the image
// img.Mutate(x =>
// {
// x.DrawText(new RichTextOptions(font)
// {
// HorizontalAlignment = HorizontalAlignment.Center,
// VerticalAlignment = VerticalAlignment.Center,
// FallbackFontFamilies = _fonts.FallBackFonts,
// Origin = new(50, 20),
// TextRuns = [strikeoutRun]
// },
// password,
// Brushes.Solid(Color.White),
// outlinePen);
// });
// using var stream = await img.ToStreamAsync();
// var captcha = await Response()
// .Embed(_sender.CreateEmbed()
// .WithOkColor()
// .WithImageUrl("attachment://timely.png"))
// .File(stream, "timely.png")
// .SendAsync();
// try
// {
// var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
// if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
// {
// return;
// }
// }
// finally
// {
// _ = captcha.DeleteAsync();
// }
var interaction = CreateTimelyInteraction();
var msg = await Response().Pending(strs.timely_button).Interaction(interaction).SendAsync();
await msg.DeleteAsync();
return;
}
await ClaimTimely();
}
private async Task ClaimTimely()
{
var period = Config.Timely.Cooldown;
if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } remainder)
{
// Get correct time form remainder
@@ -166,8 +246,9 @@ public partial class Gambling : GamblingModule<GamblingService>
await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(interaction).SendAsync();
return;
}
var val = Config.Timely.Amount;
var patron = await _ps.GetPatronAsync(ctx.User.Id);
var percentBonus = (_ps.PercentBonus(patron) / 100f);
@@ -625,8 +706,6 @@ public partial class Gambling : GamblingModule<GamblingService>
var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
// List<DiscordUser> cleanRichest;
// it's pointless to have clean on dm context
if (ctx.Guild is null)
{
opts.Clean = false;
@@ -640,13 +719,18 @@ public partial class Gambling : GamblingModule<GamblingService>
await ctx.Channel.TriggerTypingAsync();
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
var users = ((SocketGuild)ctx.Guild).Users.Map(x => x.Id);
var perPage = 9;
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>()
.GetTopRichest(_client.CurrentUser.Id, 0, 1000);
var sg = (SocketGuild)ctx.Guild!;
return cleanRichest.Where(x => sg.GetUser(x.UserId) is not null).ToList();
return cleanRichest;
}
else
{
@@ -655,13 +739,9 @@ public partial class Gambling : GamblingModule<GamblingService>
}
}
var res = Response()
.Paginated();
await Response()
.Paginated()
.PageItems(GetTopRichest)
.TotalElements(900)
.PageSize(9)
.CurrentPage(page)
.Page((toSend, curPage) =>
@@ -762,6 +842,8 @@ public partial class Gambling : GamblingModule<GamblingService>
private static readonly ImmutableArray<string> _emojis =
new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray();
private readonly NadekoRandom _rng;
[Cmd]
public async Task LuckyLadder([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)

View File

@@ -11,7 +11,7 @@ namespace NadekoBot.Modules.Gambling.Common;
public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
{
[Comment("""DO NOT CHANGE""")]
public int Version { get; set; } = 8;
public int Version { get; set; } = 9;
[Comment("""Currency settings""")]
public CurrencyConfig Currency { get; set; }
@@ -111,6 +111,11 @@ public partial class TimelyConfig
setting to 0 or less will disable this feature
""")]
public int Cooldown { get; set; } = 24;
[Comment("""
Whether the users are required to type a password when they do timely.
""")]
public bool RequirePassword { get; set; } = true;
}
[Cloneable]

View File

@@ -144,6 +144,11 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
ConfigPrinters.ToString,
val => val >= 0);
AddParsedProp("timely.pass",
gs => gs.Timely.RequirePassword,
bool.TryParse,
ConfigPrinters.ToString);
Migrate();
}
@@ -167,22 +172,6 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
});
}
if (data.Version < 5)
{
ModifyConfig(c =>
{
c.Version = 5;
});
}
if (data.Version < 6)
{
ModifyConfig(c =>
{
c.Version = 6;
});
}
if (data.Version < 7)
{
ModifyConfig(c =>
@@ -199,5 +188,13 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
c.Waifu.Decay.UnclaimedDecayPercent = 0;
});
}
if (data.Version < 9)
{
ModifyConfig(c =>
{
c.Version = 9;
});
}
}
}

View File

@@ -16,6 +16,7 @@ public class GamblingService : INService, IReadyExecutor
private readonly DiscordSocketClient _client;
private readonly IBotCache _cache;
private readonly GamblingConfigService _gss;
private readonly NadekoRandom _rng;
private static readonly TypedKey<long> _curDecayKey = new("currency:last_decay");
@@ -29,11 +30,19 @@ public class GamblingService : INService, IReadyExecutor
_client = client;
_cache = cache;
_gss = gss;
_rng = new NadekoRandom();
}
public Task OnReadyAsync()
=> Task.WhenAll(CurrencyDecayLoopAsync(), TransactionClearLoopAsync());
public string GeneratePassword()
{
var num = _rng.Next((int)Math.Pow(31, 2), (int)Math.Pow(32, 3));
return new kwum(num).ToString();
}
private async Task TransactionClearLoopAsync()
{
if (_client.ShardId != 0)
@@ -52,7 +61,7 @@ public class GamblingService : INService, IReadyExecutor
var days = TimeSpan.FromDays(lifetime);
await using var uow = _db.GetDbContext();
await uow.Set<CurrencyTransaction>()
.DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days);
.DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days);
}
catch (Exception ex)
{
@@ -90,11 +99,11 @@ public class GamblingService : INService, IReadyExecutor
}
Log.Information("""
--- Decaying users' currency ---
| decay: {ConfigDecayPercent}%
| max: {MaxDecay}
| threshold: {DecayMinTreshold}
""",
--- Decaying users' currency ---
| decay: {ConfigDecayPercent}%
| max: {MaxDecay}
| threshold: {DecayMinTreshold}
""",
config.Decay.Percent * 100,
maxDecay,
config.Decay.MinThreshold);
@@ -104,14 +113,14 @@ public class GamblingService : INService, IReadyExecutor
var decay = (double)config.Decay.Percent;
await uow.Set<DiscordUser>()
.Where(x => x.CurrencyAmount > config.Decay.MinThreshold && x.UserId != _client.CurrentUser.Id)
.UpdateAsync(old => new()
{
CurrencyAmount =
maxDecay > Sql.Round((old.CurrencyAmount * decay) - 0.5)
? (long)(old.CurrencyAmount - Sql.Round((old.CurrencyAmount * decay) - 0.5))
: old.CurrencyAmount - maxDecay
});
.Where(x => x.CurrencyAmount > config.Decay.MinThreshold && x.UserId != _client.CurrentUser.Id)
.UpdateAsync(old => new()
{
CurrencyAmount =
maxDecay > Sql.Round((old.CurrencyAmount * decay) - 0.5)
? (long)(old.CurrencyAmount - Sql.Round((old.CurrencyAmount * decay) - 0.5))
: old.CurrencyAmount - maxDecay
});
await uow.SaveChangesAsync();
@@ -133,6 +142,7 @@ public class GamblingService : INService, IReadyExecutor
private static TypedKey<Dictionary<ulong, long>> _timelyKey
= new("timely:claims");
public async Task<TimeSpan?> ClaimTimelyAsync(ulong userId, int period)
{
if (period == 0)
@@ -178,9 +188,10 @@ public class GamblingService : INService, IReadyExecutor
public bool UserHasTimelyReminder(ulong userId)
{
var db = _db.GetDbContext();
return db.GetTable<Reminder>().Any(x => x.UserId == userId
&& x.Type == ReminderType.Timely);
}
return db.GetTable<Reminder>()
.Any(x => x.UserId == userId
&& x.Type == ReminderType.Timely);
}
public async Task RemoveAllTimelyClaimsAsync()
=> await _cache.RemoveAsync(_timelyKey);

View File

@@ -1,8 +1,11 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models;
using SixLabors.Fonts;
using SixLabors.Fonts.Unicode;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
@@ -25,6 +28,7 @@ public class PlantPickService : INService, IExecNoCommand
private readonly NadekoRandom _rng;
private readonly DiscordSocketClient _client;
private readonly GamblingConfigService _gss;
private readonly GamblingService _gs;
private readonly ConcurrentHashSet<ulong> _generationChannels;
private readonly SemaphoreSlim _pickLock = new(1, 1);
@@ -37,7 +41,8 @@ public class PlantPickService : INService, IExecNoCommand
ICurrencyService cs,
CommandHandler cmdHandler,
DiscordSocketClient client,
GamblingConfigService gss)
GamblingConfigService gss,
GamblingService gs)
{
_db = db;
_strings = strings;
@@ -48,6 +53,7 @@ public class PlantPickService : INService, IExecNoCommand
_rng = new();
_client = client;
_gss = gss;
_gs = gs;
using var uow = db.GetDbContext();
var guildIds = client.Guilds.Select(x => x.Id).ToList();
@@ -87,6 +93,7 @@ public class PlantPickService : INService, IExecNoCommand
var toDelete = guildConfig.GenerateCurrencyChannelIds.FirstOrDefault(x => x.Equals(toAdd));
if (toDelete is not null)
uow.Remove(toDelete);
_generationChannels.TryRemove(cid);
enabled = false;
}
@@ -157,8 +164,26 @@ public class PlantPickService : INService, IExecNoCommand
new PointF(size.Width + 5, size.Height + 10),
new PointF(0, size.Height + 10));
var strikeoutRun = new RichTextRun
{
Start = 0,
End = pass.GetGraphemeCount(),
Font = font,
StrikeoutPen = new SolidPen(Color.White, 5),
TextDecorations = TextDecorations.Strikeout
};
// draw the password over the background
x.DrawText(pass, font, Color.White, new(0, 0));
x.DrawText(new RichTextOptions(font)
{
Origin = new(0, 0),
TextRuns =
[
strikeoutRun
]
},
pass,
new SolidBrush(Color.White));
});
// return image as a stream for easy sending
var format = img.Metadata.DecodedImageFormat;
@@ -208,7 +233,7 @@ public class PlantPickService : INService, IExecNoCommand
+ " "
+ GetText(channel.GuildId, strs.pick_pl(prefix));
var pw = config.Generation.HasPassword ? GenerateCurrencyPassword().ToUpperInvariant() : null;
var pw = config.Generation.HasPassword ? _gs.GeneratePassword().ToUpperInvariant() : null;
IUserMessage sent;
var (stream, ext) = await GetRandomCurrencyImageAsync(pw);
@@ -232,67 +257,44 @@ public class PlantPickService : INService, IExecNoCommand
return Task.CompletedTask;
}
/// <summary>
/// Generate a hexadecimal string from 1000 to ffff.
/// </summary>
/// <returns>A hexadecimal string from 1000 to ffff</returns>
private string GenerateCurrencyPassword()
{
// generate a number from 1000 to ffff
var num = _rng.Next(4096, 65536);
// convert it to hexadecimal
return num.ToString("x4");
}
public async Task<long> PickAsync(
ulong gid,
ITextChannel ch,
ulong uid,
string pass)
{
await _pickLock.WaitAsync();
long amount;
ulong[] ids;
await using (var uow = _db.GetDbContext())
{
// this method will sum all plants with that password,
// remove them, and get messageids of the removed plants
pass = pass?.Trim().TrimTo(10, true)?.ToUpperInvariant();
// gets all plants in this channel with the same password
var entries = await uow.GetTable<PlantedCurrency>()
.Where(x => x.ChannelId == ch.Id && pass == x.Password)
.DeleteWithOutputAsync();
if (!entries.Any())
return 0;
amount = entries.Sum(x => x.Amount);
ids = entries.Select(x => x.MessageId).ToArray();
}
if (amount > 0)
await _cs.AddAsync(uid, amount, new("currency", "collect"));
try
{
long amount;
ulong[] ids;
await using (var uow = _db.GetDbContext())
{
// this method will sum all plants with that password,
// remove them, and get messageids of the removed plants
pass = pass?.Trim().TrimTo(10, true).ToUpperInvariant();
// gets all plants in this channel with the same password
var entries = uow.Set<PlantedCurrency>()
.AsQueryable()
.Where(x => x.ChannelId == ch.Id && pass == x.Password)
.ToList();
// sum how much currency that is, and get all of the message ids (so that i can delete them)
amount = entries.Sum(x => x.Amount);
ids = entries.Select(x => x.MessageId).ToArray();
// remove them from the database
uow.RemoveRange(entries);
if (amount > 0)
// give the picked currency to the user
await _cs.AddAsync(uid, amount, new("currency", "collect"));
await uow.SaveChangesAsync();
}
try
{
// delete all of the plant messages which have just been picked
_ = ch.DeleteMessagesAsync(ids);
}
catch { }
// return the amount of currency the user picked
return amount;
}
finally
{
_pickLock.Release();
_ = ch.DeleteMessagesAsync(ids);
}
catch { }
// return the amount of currency the user picked
return amount;
}
public async Task<ulong?> SendPlantMessageAsync(

View File

@@ -14,13 +14,13 @@ public class VoteModel
public class VoteRewardService : INService, IReadyExecutor
{
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private readonly ICurrencyService _currencyService;
private readonly GamblingConfigService _gamb;
public VoteRewardService(
DiscordSocketClient client,
IBotCredentials creds,
IBotCreds creds,
ICurrencyService currencyService,
GamblingConfigService gamb)
{

View File

@@ -227,7 +227,7 @@ public partial class Gambling
if (page > 100)
page = 100;
var waifus = _service.GetTopWaifusAtPage(page).ToList();
var waifus = await _service.GetTopWaifusAtPage(page);
if (waifus.Count == 0)
{
@@ -252,18 +252,18 @@ public partial class Gambling
var claimer = "no one";
string status;
var waifuUsername = w.Username.TrimTo(20);
var claimerUsername = w.Claimer?.TrimTo(20);
var waifuUsername = w.WaifuName.TrimTo(20);
var claimerUsername = w.ClaimerName?.TrimTo(20);
if (w.Claimer is not null)
claimer = $"{claimerUsername}#{w.ClaimerDiscrim}";
if (w.ClaimerName is not null)
claimer = $"{claimerUsername}";
if (w.Affinity is null)
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";
else
status = $"... but {waifuUsername}'s heart belongs to {w.Affinity.TrimTo(20)}#{w.AffinityDiscrim}";
return $"**{waifuUsername}#{w.Discrim}** - claimed by **{claimer}**\n\t{status}";
status = $"... but {waifuUsername}'s heart belongs to {w.Affinity.TrimTo(20)}";
return $"**{waifuUsername}** - claimed by **{claimer}**\n\t{status}";
}
[Cmd]

View File

@@ -15,7 +15,7 @@ public class WaifuService : INService, IReadyExecutor
private readonly ICurrencyService _cs;
private readonly IBotCache _cache;
private readonly GamblingConfigService _gss;
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private readonly DiscordSocketClient _client;
public WaifuService(
@@ -23,7 +23,7 @@ public class WaifuService : INService, IReadyExecutor
ICurrencyService cs,
IBotCache cache,
GamblingConfigService gss,
IBotCredentials creds,
IBotCreds creds,
DiscordSocketClient client)
{
_db = db;
@@ -300,10 +300,10 @@ public class WaifuService : INService, IReadyExecutor
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();
return uow.Set<WaifuInfo>().GetTop(perPage, page * perPage);
await using var uow = _db.GetDbContext();
return await uow.Set<WaifuInfo>().GetTop(perPage, page * perPage);
}
public ulong GetWaifuUserId(ulong ownerId, string name)
@@ -577,7 +577,7 @@ public class WaifuService : INService, IReadyExecutor
{
await using var uow = _db.GetDbContext();
await uow.GetTable<WaifuInfo>()
.Where(x => x.Price > minPrice && x.ClaimerId == null)
.Where(x => x.Price > minPrice && x.ClaimerId != null)
.UpdateAsync(old => new()
{
Price = (long)(old.Price * claimedMulti)
@@ -603,7 +603,7 @@ public class WaifuService : INService, IReadyExecutor
.Where(wi => wi.ClaimerId == waifuId)
.Select(wi => wi.WaifuId)
.Contains(x.Id))
.Select(x => $"{x.Username}#{x.Discriminator}")
.Select(x => x.Username)
.ToListAsyncEF();
}
@@ -615,7 +615,7 @@ public class WaifuService : INService, IReadyExecutor
.Where(wi => wi.AffinityId == waifuId)
.Select(wi => wi.WaifuId)
.Contains(x.Id))
.Select(x => $"{x.Username}#{x.Discriminator}")
.Select(x => x.Username)
.ToListAsyncEF();
}

View File

@@ -25,30 +25,32 @@ public static class WaifuExtensions
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);
if (count == 0)
return [];
return waifus.Include(wi => wi.Waifu)
.Include(wi => wi.Affinity)
.Include(wi => wi.Claimer)
.OrderByDescending(wi => wi.Price)
.Skip(skip)
.Take(count)
.Select(x => new WaifuLbResult
{
Affinity = x.Affinity == null ? null : x.Affinity.Username,
AffinityDiscrim = x.Affinity == null ? null : x.Affinity.Discriminator,
Claimer = x.Claimer == null ? null : x.Claimer.Username,
ClaimerDiscrim = x.Claimer == null ? null : x.Claimer.Discriminator,
Username = x.Waifu.Username,
Discrim = x.Waifu.Discriminator,
Price = x.Price
})
.ToList();
return await waifus.Include(wi => wi.Waifu)
.Include(wi => wi.Affinity)
.Include(wi => wi.Claimer)
.OrderByDescending(wi => wi.Price)
.Skip(skip)
.Take(count)
.Select(x => new WaifuLbResult
{
Affinity = x.Affinity == null
? null
: x.Affinity.Username,
ClaimerName =
x.Claimer == null
? null
: x.Claimer.Username,
WaifuName = x.Waifu.Username,
Price = x.Price
})
.ToListAsyncEF();
}
public static decimal GetTotalValue(this DbSet<WaifuInfo> waifus)
@@ -57,14 +59,14 @@ public static class WaifuExtensions
public static ulong GetWaifuUserId(this DbSet<WaifuInfo> waifus, ulong ownerId, string name)
=> waifus.AsQueryable()
.AsNoTracking()
.Where(x => x.Claimer.UserId == ownerId && x.Waifu.Username + "#" + x.Waifu.Discriminator == name)
.Where(x => x.Claimer.UserId == ownerId && x.Waifu.Username == name)
.Select(x => x.Waifu.UserId)
.FirstOrDefault();
public static async Task<WaifuInfoStats> GetWaifuInfoAsync(this DbContext ctx, ulong userId)
{
await ctx.EnsureUserCreatedAsync(userId);
await ctx.Set<WaifuInfo>()
.ToLinqToDBTable()
.InsertOrUpdateAsync(() => new()
@@ -95,7 +97,7 @@ public static class WaifuExtensions
ctx.Set<DiscordUser>()
.AsQueryable()
.Where(u => u.UserId == userId)
.Select(u => u.Username + "#" + u.Discriminator)
.Select(u => u.Username)
.FirstOrDefault(),
AffinityCount =
ctx.Set<WaifuUpdate>()
@@ -107,14 +109,14 @@ public static class WaifuExtensions
ctx.Set<DiscordUser>()
.AsQueryable()
.Where(u => u.Id == w.AffinityId)
.Select(u => u.Username + "#" + u.Discriminator)
.Select(u => u.Username)
.FirstOrDefault(),
ClaimCount = ctx.Set<WaifuInfo>().AsQueryable().Count(x => x.ClaimerId == w.WaifuId),
ClaimerName =
ctx.Set<DiscordUser>()
.AsQueryable()
.Where(u => u.Id == w.ClaimerId)
.Select(u => u.Username + "#" + u.Discriminator)
.Select(u => u.Username)
.FirstOrDefault(),
DivorceCount =
ctx.Set<WaifuUpdate>()

View File

@@ -3,14 +3,11 @@ namespace NadekoBot.Db.Models;
public class WaifuLbResult
{
public string Username { get; set; }
public string Discrim { get; set; }
public string WaifuName { get; set; }
public string Claimer { get; set; }
public string ClaimerDiscrim { get; set; }
public string ClaimerName { get; set; }
public string Affinity { get; set; }
public string AffinityDiscrim { get; set; }
public long Price { get; set; }
}

View File

@@ -19,7 +19,7 @@ public class ChatterBotService : IExecOnMessage
private readonly DiscordSocketClient _client;
private readonly IPermissionChecker _perms;
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private readonly IHttpClientFactory _httpFactory;
private readonly GamesConfigService _gcs;
private readonly IMessageSenderService _sender;
@@ -32,7 +32,7 @@ public class ChatterBotService : IExecOnMessage
IBot bot,
IPatronageService ps,
IHttpClientFactory factory,
IBotCredentials creds,
IBotCreds creds,
GamesConfigService gcs,
IMessageSenderService sender,
DbService db)

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

View File

@@ -0,0 +1,305 @@
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;
}
var result = await _service.SetPixel(position, clr.PackedValue, text, ctx.User.Id, pixel.Price);
if (result == SetPixelResult.NotEnoughMoney)
{
await Response().Error(strs.not_enough(_gcs.Data.Currency.Sign)).SendAsync();
return;
}
else if (result == SetPixelResult.InsufficientPayment)
{
await Response().Error(strs.nc_insuff_payment).SendAsync();
return;
}
else if (result == SetPixelResult.InvalidInput)
{
await Response().Error(strs.invalid_input).SendAsync();
return;
}
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();
}
}
}

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

View File

@@ -0,0 +1,9 @@
namespace NadekoBot.Modules.Games;
public enum SetPixelResult
{
Success,
InsufficientPayment,
NotEnoughMoney,
InvalidInput
}

View File

@@ -29,7 +29,7 @@ public partial class Games
if (!await nunchi.Join(ctx.User.Id, ctx.User.ToString()))
return;
await Response().Error(strs.nunchi_joined(nunchi.ParticipantCount)).SendAsync();
await Response().Confirm(strs.nunchi_joined(nunchi.ParticipantCount)).SendAsync();
return;
}

View File

@@ -12,9 +12,9 @@ public sealed partial class Music
{
private static readonly SemaphoreSlim _playlistLock = new(1, 1);
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;
_creds = creds;

View File

@@ -43,8 +43,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
+ "--no-check-certificate "
+ "-i "
+ "--yes-playlist "
+ "-- \"{0}\"",
scs.Data.YtProvider != YoutubeSearcher.Ytdl);
+ "-- \"{0}\"");
_ytdlIdOperation = new("-4 "
+ "--geo-bypass "
@@ -56,8 +55,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
+ "--get-thumbnail "
+ "--get-duration "
+ "--no-check-certificate "
+ "-- \"{0}\"",
scs.Data.YtProvider != YoutubeSearcher.Ytdl);
+ "-- \"{0}\"");
_ytdlSearchOperation = new("-4 "
+ "--geo-bypass "
@@ -70,8 +68,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
+ "--get-duration "
+ "--no-check-certificate "
+ "--default-search "
+ "\"ytsearch:\" -- \"{0}\"",
scs.Data.YtProvider != YoutubeSearcher.Ytdl);
+ "\"ytsearch:\" -- \"{0}\"");
}
private YtTrackData ResolveYtdlData(string ytdlOutputString)
@@ -291,6 +288,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
public Task<string?> GetStreamUrl(string videoId)
=> CreateCacherFactory(videoId)();
private readonly struct YtTrackData
{
public readonly string Title;

View File

@@ -122,11 +122,11 @@ public sealed class CurrencyRewardService : INService, IReadyExecutor
var dollarValue = pledgeCents / 100;
percentBonus = dollarValue switch
{
>= 100 => 100,
>= 50 => 50,
>= 20 => 20,
>= 10 => 10,
>= 5 => 5,
>= 100 => 20,
>= 50 => 10,
>= 20 => 5,
>= 10 => 3,
>= 5 => 1,
_ => 0
};
return (long)(modifiedAmount * (1 + (percentBonus / 100.0f)));

View File

@@ -67,7 +67,7 @@ public partial class Permissions
return _sender.CreateEmbed()
.WithTitle(title)
.WithDescription(allItems.Join('\n'))
.WithDescription(pageItems.Join('\n'))
.WithOkColor();
})
.SendAsync();

View File

@@ -16,11 +16,11 @@ public class CryptoService : INService
{
private readonly IBotCache _cache;
private readonly IHttpClientFactory _httpFactory;
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
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;
_httpFactory = httpFactory;

View File

@@ -9,10 +9,10 @@ public partial class Searches
[Group]
public partial class OsuCommands : NadekoModule<OsuService>
{
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private readonly IHttpClientFactory _httpFactory;
public OsuCommands(IBotCredentials creds, IHttpClientFactory factory)
public OsuCommands(IBotCreds creds, IHttpClientFactory factory)
{
_creds = creds;
_httpFactory = factory;

View File

@@ -7,9 +7,9 @@ namespace NadekoBot.Modules.Searches;
public sealed class OsuService : INService
{
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;
_creds = creds;

View File

@@ -7,10 +7,9 @@ public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, INServi
{
private readonly SearchesConfigService _scs;
private readonly SearxSearchService _sss;
private readonly YtDlpSearchService _ytdlp;
private readonly GoogleSearchService _gss;
private readonly YtdlpYoutubeSearchService _ytdlp;
private readonly YtdlYoutubeSearchService _ytdl;
private readonly YoutubeDataApiSearchService _ytdata;
private readonly InvidiousYtSearchService _iYtSs;
private readonly GoogleScrapeService _gscs;
@@ -20,19 +19,17 @@ public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, INServi
GoogleSearchService gss,
GoogleScrapeService gscs,
SearxSearchService sss,
YtdlpYoutubeSearchService ytdlp,
YtdlYoutubeSearchService ytdl,
YtDlpSearchService ytdlp,
YoutubeDataApiSearchService ytdata,
InvidiousYtSearchService iYtSs)
{
_scs = scs;
_sss = sss;
_ytdlp = ytdlp;
_gss = gss;
_gscs = gscs;
_iYtSs = iYtSs;
_ytdlp = ytdlp;
_ytdl = ytdl;
_ytdata = ytdata;
}
@@ -57,9 +54,8 @@ public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, INServi
=> _scs.Data.YtProvider switch
{
YoutubeSearcher.YtDataApiv3 => _ytdata,
YoutubeSearcher.Ytdlp => _ytdlp,
YoutubeSearcher.Ytdl => _ytdl,
YoutubeSearcher.Invidious => _iYtSs,
_ => _ytdl
YoutubeSearcher.Ytdlp => _ytdlp,
_ => throw new ArgumentOutOfRangeException()
};
}

View File

@@ -93,16 +93,12 @@ public partial class Searches
return;
}
var embeds = new List<EmbedBuilder>(4);
EmbedBuilder CreateEmbed(IImageSearchResultEntry entry)
{
return _sender.CreateEmbed()
.WithOkColor()
.WithAuthor(ctx.User)
.WithTitle(query)
.WithUrl("https://google.com")
.WithImageUrl(entry.Link);
}
@@ -120,55 +116,50 @@ public partial class Searches
.WithDescription(GetText(strs.no_search_results));
var embed = CreateEmbed(item);
embeds.Add(embed);
return embed;
})
.SendAsync();
}
private TypedKey<string> GetYtCacheKey(string query)
=> new($"search:youtube:{query}");
private TypedKey<string[]> GetYtCacheKey(string 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());
private async Task<VideoInfo?> GetYoutubeUrlFromCacheAsync(string query)
private async Task<VideoInfo[]?> GetYoutubeUrlFromCacheAsync(string 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 new VideoInfo()
return urls.Map(url => new VideoInfo()
{
Url = url
};
});
}
[Cmd]
public async Task Youtube([Leftover] string? query = null)
public async Task Youtube([Leftover] string query)
{
query = query?.Trim();
if (string.IsNullOrWhiteSpace(query))
{
await Response().Error(strs.specify_search_params).SendAsync();
return;
}
query = query.Trim();
_ = ctx.Channel.TriggerTypingAsync();
var maybeResult = await GetYoutubeUrlFromCacheAsync(query)
?? await _searchFactory.GetYoutubeSearchService().SearchAsync(query);
if (maybeResult is not { } result || result is { Url: null })
var maybeResults = await GetYoutubeUrlFromCacheAsync(query)
?? await _searchFactory.GetYoutubeSearchService().SearchAsync(query);
if (maybeResults is not { } result || result.Length == 0)
{
await Response().Error(strs.no_results).SendAsync();
return;
}
await AddYoutubeUrlToCacheAsync(query, result.Url);
await Response().Text(result.Url).SendAsync();
await AddYoutubeUrlToCacheAsync(query, result.Map(x => x.Url));
await Response().Text(result[0].Url).SendAsync();
}
// [Cmd]

View File

@@ -2,5 +2,5 @@
public interface IYoutubeSearchService
{
Task<VideoInfo?> SearchAsync(string query);
Task<VideoInfo[]?> SearchAsync(string query);
}

View File

@@ -18,7 +18,7 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, INService
_rng = new();
}
public async Task<VideoInfo?> SearchAsync(string query)
public async Task<VideoInfo[]?> SearchAsync(string query)
{
ArgumentNullException.ThrowIfNull(query);
@@ -35,6 +35,7 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, INService
var url = $"{instance}/api/v1/search"
+ $"?q={query}"
+ $"&type=video";
using var http = _http.CreateClient();
var res = await http.GetFromJsonAsync<List<InvidiousSearchResponse>>(
url);
@@ -42,6 +43,6 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, INService
if (res is null or { Count: 0 })
return null;
return new VideoInfo(res[0].VideoId);
return res.Map(r => new VideoInfo(r.VideoId));
}
}

View File

@@ -9,18 +9,18 @@ public sealed class YoutubeDataApiSearchService : IYoutubeSearchService, INServi
_gapi = gapi;
}
public async Task<VideoInfo?> SearchAsync(string query)
public async Task<VideoInfo[]?> SearchAsync(string query)
{
ArgumentNullException.ThrowIfNull(query);
var results = await _gapi.GetVideoLinksByKeywordAsync(query);
var first = results.FirstOrDefault();
if (first is null)
if(results.Count == 0)
return null;
return new()
return results.Map(r => new VideoInfo()
{
Url = first
};
Url = r
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,14 +13,14 @@ namespace NadekoBot.Modules.Searches;
public partial class Searches : NadekoModule<SearchesService>
{
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private readonly IGoogleApiService _google;
private readonly IHttpClientFactory _httpFactory;
private readonly IMemoryCache _cache;
private readonly ITimezoneService _tzSvc;
public Searches(
IBotCredentials creds,
IBotCreds creds,
IGoogleApiService google,
IHttpClientFactory factory,
IMemoryCache cache,

View File

@@ -0,0 +1,191 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models;
using System.Collections.Frozen;
namespace NadekoBot.Modules.Searches;
public sealed partial class FlagTranslateService : IReadyExecutor, INService
{
private readonly IBotCreds _creds;
private readonly DiscordSocketClient _client;
private readonly TranslateService _ts;
private readonly IMessageSenderService _sender;
private IReadOnlyDictionary<string, string> _supportedFlags;
private readonly DbService _db;
private ConcurrentHashSet<ulong> _enabledChannels;
private readonly IBotCache _cache;
// disallow same message being translated multiple times to the same language
private readonly ConcurrentHashSet<(ulong, string)> _msgLangs = new();
public FlagTranslateService(
IBotCreds creds,
DiscordSocketClient client,
TranslateService ts,
IMessageSenderService sender,
DbService db,
IBotCache cache)
{
_creds = creds;
_client = client;
_ts = ts;
_sender = sender;
_db = db;
_cache = cache;
}
public async Task OnReadyAsync()
{
_supportedFlags = COUNTRIES
.Split('\n')
.Select(x => x.Split(' '))
.ToDictionary(x => x[0], x => x[1].TrimEnd())
.ToFrozenDictionary();
await using (var uow = _db.GetDbContext())
{
_enabledChannels = (await uow.GetTable<FlagTranslateChannel>()
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId,
_creds.TotalShards,
_client.ShardId))
.Select(x => new
{
x.ChannelId,
x.GuildId
})
.ToListAsyncLinqToDB())
.Select(x => x.ChannelId)
.ToHashSet()
.ToConcurrentSet();
}
_client.ReactionAdded += OnReactionAdded;
var periodicCleanup = new PeriodicTimer(TimeSpan.FromHours(24));
while (await periodicCleanup.WaitForNextTickAsync())
{
_msgLangs.Clear();
}
}
private const int FLAG_START = 127462;
private static TypedKey<bool> CdKey(ulong userId)
=> new($"flagtranslate:{userId}");
private Task OnReactionAdded(
Cacheable<IUserMessage, ulong> arg1,
Cacheable<IMessageChannel, ulong> arg2,
SocketReaction reaction)
{
if (!_enabledChannels.Contains(reaction.Channel.Id))
return Task.CompletedTask;
var runes = reaction.Emote.Name.EnumerateRunes();
if (!runes.MoveNext()
|| runes.Current is not { Value: >= 127462 and <= 127487 } l1
|| !runes.MoveNext()
|| runes.Current is not { Value: >= 127462 and <= 127487 } l2)
{
return Task.CompletedTask;
}
_ = Task.Run(async () =>
{
if (reaction.Channel is not SocketTextChannel tc)
return;
var user = await ((IGuild)tc.Guild).GetUserAsync(reaction.UserId);
if (user is null)
return;
if (!user.GetPermissions(tc).SendMessages)
return;
if (!tc.Guild.CurrentUser.GetPermissions(tc).SendMessages
|| !tc.Guild.CurrentUser.GetPermissions(tc).EmbedLinks)
{
await Disable(tc.Guild.Id, tc.Id);
return;
}
var c1 = (char)(l1.Value - FLAG_START + 65);
var c2 = (char)(l2.Value - FLAG_START + 65);
var code = $"{c1}{c2}".ToUpper();
if (!_supportedFlags.TryGetValue(code, out var lang))
return;
if (!_msgLangs.Add((reaction.MessageId, lang)))
return;
var result = await _cache.GetAsync(CdKey(reaction.UserId));
if (result.TryPickT0(out _, out _))
return;
await _cache.AddAsync(CdKey(reaction.UserId), true, TimeSpan.FromSeconds(5));
var msg = await arg1.GetOrDownloadAsync();
var response = await _ts.Translate("", lang, msg.Content).ConfigureAwait(false);
await msg.ReplyAsync(embed: _sender.CreateEmbed()
.WithOkColor()
.WithFooter(user.ToString() ?? reaction.UserId.ToString(),
user.RealAvatarUrl().ToString())
.WithDescription(response)
.WithAuthor(reaction.Emote.ToString())
.Build(),
allowedMentions: AllowedMentions.None
);
});
return Task.CompletedTask;
}
public async Task Disable(ulong guildId, ulong tcId)
{
if (!_enabledChannels.TryRemove(tcId))
return;
await using var uow = _db.GetDbContext();
await uow.GetTable<FlagTranslateChannel>()
.Where(x => x.GuildId == guildId
&& x.ChannelId == tcId)
.DeleteAsync();
}
public async Task<bool> Toggle(ulong guildId, ulong tcId)
{
if (_enabledChannels.Contains(tcId))
{
await Disable(guildId, tcId);
return false;
}
await Enable(guildId, tcId);
return true;
}
public async Task Enable(ulong guildId, ulong tcId)
{
if (!_enabledChannels.Add(tcId))
return;
await using var uow = _db.GetDbContext();
await uow.GetTable<FlagTranslateChannel>()
.InsertAsync(() => new FlagTranslateChannel
{
GuildId = guildId,
ChannelId = tcId
});
}
}

View File

@@ -0,0 +1,73 @@
namespace NadekoBot.Modules.Searches;
public partial class FlagTranslateService
{
private const string COUNTRIES = """
CN zh
IN hi
US en
ID id
PK ur
BR pt
NG ha
BD bn
RU ru
JP ja
MX es
PH tl
VN vi
EG ar
ET am
DE de
IR fa
TR tr
TH th
FR fr
CD fr
MM my
UG en
MZ pt
ZA zu
CO es
BG bg
HR hr
MY ms
NL nl
RO ro
CZ cs
GR el
SK sk
PT pt
KR ko
IT it
ES es
RS sr
TN ar
PL pl
SD ar
CM fr
SN fr
ML fr
NE ha
BI fr
AO pt
AF ps
MA ar
DZ ar
GB en
AR es
ZW ny
KE sw
GH en
SA ar
IL he
IQ ar
UA ua
LY ar
KW ar
OM ar
YE ar
AL sq
AE ar
""";
}

View File

@@ -44,12 +44,10 @@ public sealed class TranslateService : ITranslateService, IExecNoCommand, IReady
foreach (var c in cs)
{
_atcs[c.ChannelId] = c.AutoDelete;
_users[c.ChannelId] =
new(c.Users.ToDictionary(x => x.UserId, x => (x.Source.ToLower(), x.Target.ToLower())));
_users[c.ChannelId] = new(c.Users.ToDictionary(x => x.UserId, x => (x.Source.ToLower(), x.Target.ToLower())));
}
}
public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
{
if (string.IsNullOrWhiteSpace(msg.Content))
@@ -95,7 +93,7 @@ public sealed class TranslateService : ITranslateService, IExecNoCommand, IReady
}
}
public async Task<string> Translate(string source, string target, string text = null)
public async Task<string> Translate(string source, string target, string text)
{
if (string.IsNullOrWhiteSpace(text))
throw new ArgumentException("Text is empty or null", nameof(text));

View File

@@ -6,6 +6,14 @@ public partial class Searches
[Group]
public partial class TranslateCommands : NadekoModule<ITranslateService>
{
private readonly FlagTranslateService _flagSvc;
public TranslateCommands(FlagTranslateService flagSvc)
{
_flagSvc = flagSvc;
}
public enum AutoDeleteAutoTranslate
{
Del,
@@ -91,5 +99,18 @@ public partial class Searches
await Response().Embed(eb).SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(ChannelPermission.ManageChannels)]
[BotPerm(ChannelPermission.SendMessages | ChannelPermission.EmbedLinks)]
public async Task TranslateFlags()
{
var enabled = await _flagSvc.Toggle(ctx.Guild.Id, ctx.Channel.Id);
if (enabled)
await Response().Confirm(strs.trfl_enabled).SendAsync();
else
await Response().Confirm(strs.trfl_disabled).SendAsync();
}
}
}

View File

@@ -28,11 +28,9 @@ public partial class SearchesConfig : ICloneable<SearchesConfig>
[Comment("""
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` - recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables
- `ytdlp` - default, 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
""")]
@@ -77,9 +75,9 @@ public sealed class FollowedStreamConfig
public enum YoutubeSearcher
{
YtDataApiv3,
Ytdl,
Ytdlp,
Invid,
YtDataApiv3 = 0,
Ytdl = 1,
Ytdlp = 1,
Invid = 3,
Invidious = 3
}

View File

@@ -47,19 +47,11 @@ public class SearchesConfigService : ConfigServiceBase<SearchesConfig>
});
}
if (data.Version < 2)
if (data.Version < 4)
{
ModifyConfig(c =>
{
c.Version = 2;
});
}
if (data.Version < 3)
{
ModifyConfig(c =>
{
c.Version = 3;
c.Version = 4;
});
}
}

View File

@@ -100,10 +100,6 @@ public sealed class AiAssistantService
using var client = _httpFactory.CreateClient();
// todo customize according to the bot's config
// - CurrencyName
// -
using var response = await client.SendAsync(request);
if (response.StatusCode == HttpStatusCode.TooManyRequests)

View File

@@ -11,7 +11,7 @@ public sealed class GiveawayService : INService, IReadyExecutor
public static string GiveawayEmoji = "🎉";
private readonly DbService _db;
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private readonly DiscordSocketClient _client;
private readonly IMessageSenderService _sender;
private readonly IBotStrings _strings;
@@ -20,7 +20,7 @@ public sealed class GiveawayService : INService, IReadyExecutor
private SortedSet<GiveawayModel> _giveawayCache = new SortedSet<GiveawayModel>();
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)
{
_db = db;

View File

@@ -26,6 +26,8 @@ public interface IQuoteService
ulong guildId,
string? keyword,
string text);
Task<(IReadOnlyCollection<Quote> quotes, int totalCount)> FindQuotesAsync(ulong guildId, string query, int page);
Task<IReadOnlyCollection<Quote>> GetGuildQuotesAsync(ulong guildId);
Task<int> RemoveAllByKeyword(ulong guildId, string keyword);
@@ -39,6 +41,7 @@ public interface IQuoteService
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(
ulong guildId,

View File

@@ -169,6 +169,23 @@ public sealed class QuoteService : IQuoteService, INService
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(
ulong guildId,
ulong authorId,
@@ -219,4 +236,24 @@ public sealed class QuoteService : IQuoteService, INService
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());
}
}

View File

@@ -17,14 +17,14 @@ public class RemindService : INService, IReadyExecutor, IRemindService
private readonly DiscordSocketClient _client;
private readonly DbService _db;
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private readonly IMessageSenderService _sender;
private readonly CultureInfo _culture;
public RemindService(
DiscordSocketClient client,
DbService db,
IBotCredentials creds,
IBotCreds creds,
IMessageSenderService sender)
{
_client = client;
@@ -197,26 +197,29 @@ public class RemindService : INService, IReadyExecutor, IRemindService
var st = SmartText.CreateFrom(r.Message);
var res = _sender.Response(ch)
.UserBasedMentions(_client.GetGuild(r.ServerId)?.GetUser(r.UserId));
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)
{
await _sender.Response(ch).Embeds(seta.GetEmbedBuilders()).SendAsync();
await res.Embeds(seta.GetEmbedBuilders()).SendAsync();
}
else
{
await _sender.Response(ch)
.Embed(_sender.CreateEmbed()
.WithOkColor()
.WithTitle("Reminder")
.AddField("Created At",
r.DateAdded.HasValue ? r.DateAdded.Value.ToLongDateString() : "?")
.AddField("By",
(await ch.GetUserAsync(r.UserId))?.ToString() ?? r.UserId.ToString()))
.Text(r.Message)
.SendAsync();
await res
.Embed(_sender.CreateEmbed()
.WithOkColor()
.WithTitle("Reminder")
.AddField("Created At",
r.DateAdded.HasValue ? r.DateAdded.Value.ToLongDateString() : "?")
.AddField("By",
(await ch.GetUserAsync(r.UserId))?.ToString() ?? r.UserId.ToString()))
.Text(r.Message)
.SendAsync();
}
}
catch (Exception ex)

View File

@@ -12,7 +12,7 @@ public sealed class RepeaterService : IReadyExecutor, INService
private readonly DbService _db;
private readonly IReplacementService _repSvc;
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private readonly DiscordSocketClient _client;
private readonly LinkedList<RunningRepeater> _repeaterQueue;
private readonly ConcurrentHashSet<int> _noRedundant;
@@ -25,7 +25,7 @@ public sealed class RepeaterService : IReadyExecutor, INService
DiscordSocketClient client,
DbService db,
IReplacementService repSvc,
IBotCredentials creds,
IBotCreds creds,
IMessageSenderService sender)
{
_db = db;

View File

@@ -34,7 +34,7 @@ public partial class Utility : NadekoModule
private readonly DiscordSocketClient _client;
private readonly ICoordinator _coord;
private readonly IStatsService _stats;
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private readonly DownloadTracker _tracker;
private readonly IHttpClientFactory _httpFactory;
private readonly VerboseErrorsService _veService;
@@ -45,7 +45,7 @@ public partial class Utility : NadekoModule
DiscordSocketClient client,
ICoordinator coord,
IStatsService stats,
IBotCredentials creds,
IBotCreds creds,
DownloadTracker tracker,
IHttpClientFactory httpFactory,
VerboseErrorsService veService,

View File

@@ -107,7 +107,7 @@ public partial class Xp : NadekoModule<XpService>
[Cmd]
[UserPerm(GuildPerm.ManageChannels)]
[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)
channel = ctx.Channel;
@@ -182,29 +182,28 @@ public partial class Xp : NadekoModule<XpService>
var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
await ctx.Channel.TriggerTypingAsync();
var socketGuild = (SocketGuild)ctx.Guild;
var allCleanUsers = new List<UserXpStats>();
if (opts.Clean)
{
await ctx.Channel.TriggerTypingAsync();
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
? Response()
.Paginated()
.Items(allCleanUsers)
: Response()
.Paginated()
.PageItems((curPage) => _service.GetUserXps(ctx.Guild.Id, curPage));
async Task<IReadOnlyCollection<UserXpStats>> GetPageItems(int curPage)
{
var socketGuild = (SocketGuild)ctx.Guild;
if (opts.Clean)
{
return await _service.GetGuildUserXps(ctx.Guild.Id,
socketGuild.Users.Select(x => x.Id).ToList(),
curPage);
}
await res
.PageSize(9)
return await _service.GetGuildUserXps(ctx.Guild.Id, curPage);
}
await Response()
.Paginated()
.PageItems(GetPageItems)
.PageSize(10)
.CurrentPage(page)
.Page((users, curPage) =>
{
@@ -237,15 +236,33 @@ public partial class Xp : NadekoModule<XpService>
[Cmd]
[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)
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()
.Paginated()
.PageItems(async curPage => await _service.GetUserXps(curPage))
.PageSize(9)
.PageItems(GetPageItems)
.PageSize(10)
.Page((users, curPage) =>
{
var embed = _sender.CreateEmbed()
@@ -282,7 +299,9 @@ public partial class Xp : NadekoModule<XpService>
if (role.IsManaged)
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()
.Confirm(
strs.xpadd_users(Format.Bold(amount.ToString()), Format.Bold(count.ToString())))
@@ -338,7 +357,7 @@ public partial class Xp : NadekoModule<XpService>
if (!await PromptUserConfirmAsync(embed))
return;
_service.XpReset(ctx.Guild.Id, userId);
await _service.XpReset(ctx.Guild.Id, userId);
await Response().Confirm(strs.reset_user(userId)).SendAsync();
}

View File

@@ -12,6 +12,7 @@ using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System.Threading.Channels;
using LinqToDB.EntityFrameworkCore;
using LinqToDB.Tools;
using NadekoBot.Modules.Patronage;
using Color = SixLabors.ImageSharp.Color;
using Exception = System.Exception;
@@ -19,13 +20,38 @@ using Image = SixLabors.ImageSharp.Image;
namespace NadekoBot.Modules.Xp.Services;
public interface IUserService
{
Task<DiscordUser?> GetUserAsync(ulong userId);
}
public sealed class UserService : IUserService, INService
{
private readonly DbService _db;
public UserService(DbService db)
{
_db = db;
}
public async Task<DiscordUser> GetUserAsync(ulong userId)
{
await using var uow = _db.GetDbContext();
var user = await uow
.GetTable<DiscordUser>()
.FirstOrDefaultAsyncLinqToDB(u => u.UserId == userId);
return user;
}
}
public class XpService : INService, IReadyExecutor, IExecNoCommand
{
private readonly DbService _db;
private readonly IImageCache _images;
private readonly IBotStrings _strings;
private readonly FontProvider _fonts;
private readonly IBotCredentials _creds;
private readonly IBotCreds _creds;
private readonly ICurrencyService _cs;
private readonly IHttpClientFactory _httpFactory;
private readonly XpConfigService _xpConfig;
@@ -55,7 +81,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
IImageCache images,
IBotCache c,
FontProvider fonts,
IBotCredentials creds,
IBotCreds creds,
ICurrencyService cs,
IHttpClientFactory http,
XpConfigService xpConfig,
@@ -563,23 +589,50 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
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();
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();
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();
return uow.Set<DiscordUser>()
.GetUsersXpLeaderboardFor(page, perPage);
await using var uow = _db.GetDbContext();
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)
@@ -1409,11 +1462,11 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
}
}
public void XpReset(ulong guildId, ulong userId)
public async Task XpReset(ulong guildId, ulong userId)
{
using var uow = _db.GetDbContext();
uow.Set<UserXpStats>().ResetGuildUserXp(userId, guildId);
uow.SaveChanges();
await using var uow = _db.GetDbContext();
await uow.GetTable<UserXpStats>()
.DeleteAsync(x => x.UserId == userId && x.GuildId == guildId);
}
public void XpReset(ulong guildId)
@@ -1609,6 +1662,15 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
public bool IsShopEnabled()
=> _xpConfig.Data.Shop.IsEnabled;
public async Task<int> GetTotalGuildUsers(ulong requestGuildId, List<ulong>? guildUsers = null)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<UserXpStats>()
.Where(x => x.GuildId == requestGuildId
&& (guildUsers == null || guildUsers.Contains(x.UserId)))
.CountAsyncLinqToDB();
}
}
public enum BuyResult

Some files were not shown because too many files have changed in this diff Show More