Compare commits

..

28 Commits

Author SHA1 Message Date
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
113 changed files with 9412 additions and 780 deletions

View File

@@ -2,6 +2,75 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [5.1.15] - 21.10.2024
## Added
- Added -c option for `.xpglb`
-
## Change
- Leaderboards will now show 10 users per page
- A lot of internal changes and improvements
## Fixed
- Fixed a big issue which caused several features to not get loaded on bot restart
- Alias collision fix `.qse` is now quotesearch, `.qs` will stay `.queuesearch`
- Fixed some migrations which would prevent users from updating from ancient versions
- Waifulb will no longer show #0000 discrims
- More `.greet` command fixes
- Author name will now be counted as content in embeds. Embeds can now only have author fields and still be valid
- Grpc api fixes, and additions
## [5.1.14] - 03.10.2024
## Changed
- Improved `.xplb -c`, it will now correctly only show users who are still in the server with no count limit
## Fixed
- Fixed medusa load error on startup
## [5.1.13] - 03.10.2024
### Fixed
- Grpc api server will no longer start unless enabled in creds
- Seq comment in creds fixed
## [5.1.12] - 03.10.2024
### Added
- Added support for `seq` for logging. If you fill in seq url and apiKey in creds.yml, bot will sends logs to it
### Fixed
- Fixed another bug in `.greet` / `.bye` system, which caused it to show wrong message on a wrong server occasionally
## [5.1.11] - 03.10.2024
### Added
- Added `%user.displayname%` placeholder. It will show users nickname, if there is one, otherwise it will show the username.
- Nickname won't be shown in bye messages.
- Added initial version of grpc api. Beta
### Fixed
- Fixed a bug which caused `.bye` and `.greet` messages to be randomly disabled
- Fixed `.lb -c` breaking sometimes, and fixed pagination
### Changed
- Youtube now always uses `yt-dlp`. Dropped support for `youtube-dl`
- If you've previously renamed your yt-dlp file to youtube-dl, please rename it back.
- ytProvider in data/searches.yml now also controls where you're getting your song streams from.
- (Invidious support added for .q)
## [5.1.10] - 24.09.2024 ## [5.1.10] - 24.09.2024
### Fixed ### Fixed

View File

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

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> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" PrivateAssets="all" GeneratePathProperty="true" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" PrivateAssets="all" GeneratePathProperty="true" />
</ItemGroup> </ItemGroup>

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,26 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
package econ;
service GrpcEcon {
rpc GetEconomy(EconomyRequest) returns (EconomyReply);
}
message EconomyRequest {
string guildId = 1;
}
message EconomyReply {
uint64 totalOwned = 1;
uint64 byTopOnePercent = 2;
uint64 plantedAmount = 3;
uint64 ownedByTheBot = 4;
uint64 inTheBank = 5;
uint64 totalEconomy = 6;
}
message CurrencyLbRequest {
int32 page = 1;
}

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,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,154 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
package other;
service GrpcOther {
rpc BotOnGuild(BotOnGuildRequest) returns (BotOnGuildReply);
rpc GetGuilds(google.protobuf.Empty) returns (GetGuildsReply);
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 GetShardStatuses(google.protobuf.Empty) returns (GetShardStatusesReply);
rpc GetServerInfo(ServerInfoRequest) returns (GetServerInfoReply);
}
message GetRolesRequest {
uint64 guildId = 1;
}
message GetRolesReply {
repeated RoleReply roles = 1;
}
message BotOnGuildRequest {
uint64 guildId = 1;
}
message BotOnGuildReply {
bool success = 1;
}
message GetGuildsReply {
repeated GuildReply guilds = 1;
}
message GuildReply {
uint64 id = 1;
string name = 2;
string iconUrl = 3;
}
message GetShardStatusesReply {
repeated ShardStatusReply shards = 1;
}
message ShardStatusReply {
int32 id = 1;
string status = 2;
int32 guildCount = 3;
google.protobuf.Timestamp lastUpdate = 4;
}
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

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

View File

@@ -87,14 +87,7 @@ public static class DiscordUserExtensions
> users.AsQueryable().Where(y => y.UserId == id).Select(y => y.TotalXp).FirstOrDefault()) > users.AsQueryable().Where(y => y.UserId == id).Select(y => y.TotalXp).FirstOrDefault())
.Count() .Count()
+ 1; + 1;
public static async Task<IReadOnlyCollection<DiscordUser>> GetUsersXpLeaderboardFor(this DbSet<DiscordUser> users, int page, int perPage)
=> await users.ToLinqToDBTable()
.OrderByDescending(x => x.TotalXp)
.Skip(page * perPage)
.Take(perPage)
.ToArrayAsyncLinqToDB();
public static Task<List<DiscordUser>> GetTopRichest( public static Task<List<DiscordUser>> GetTopRichest(
this DbSet<DiscordUser> users, this DbSet<DiscordUser> users,
ulong botId, ulong botId,

View File

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

View File

@@ -26,17 +26,6 @@ public static class UserXpExtensions
return usr; return usr;
} }
public static async Task<IReadOnlyCollection<UserXpStats>> GetUsersFor(
this DbSet<UserXpStats> xps,
ulong guildId,
int page)
=> await xps.ToLinqToDBTable()
.Where(x => x.GuildId == guildId)
.OrderByDescending(x => x.Xp + x.AwardedXp)
.Skip(page * 9)
.Take(9)
.ToArrayAsyncLinqToDB();
public static async Task<List<UserXpStats>> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count) public static async Task<List<UserXpStats>> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count)
=> await xps.ToLinqToDBTable() => await xps.ToLinqToDBTable()
.Where(x => x.GuildId == guildId) .Where(x => x.GuildId == guildId)

View File

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

View File

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

View File

@@ -62,7 +62,6 @@ public abstract class NadekoContext : DbContext
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; } public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
public DbSet<HoneypotChannel> HoneyPotChannels { get; set; } public DbSet<HoneypotChannel> HoneyPotChannels { get; set; }
// todo add guild colors
// public DbSet<GuildColors> GuildColors { get; set; } // public DbSet<GuildColors> GuildColors { get; set; }
@@ -195,11 +194,6 @@ public abstract class NadekoContext : DbContext
.WithOne() .WithOne()
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<GuildConfig>()
.HasMany(x => x.WarnPunishments)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<GuildConfig>() modelBuilder.Entity<GuildConfig>()
.HasMany(x => x.SlowmodeIgnoredRoles) .HasMany(x => x.SlowmodeIgnoredRoles)
.WithOne() .WithOne()
@@ -277,6 +271,18 @@ public abstract class NadekoContext : DbContext
#endregion #endregion
#region WarningPunishments
var warnpunishmentEntity = modelBuilder.Entity<WarningPunishment>(b =>
{
b.HasAlternateKey(x => new
{
x.GuildId,
x.Count
});
});
#endregion
#region Self Assignable Roles #region Self Assignable Roles
@@ -339,6 +345,7 @@ public abstract class NadekoContext : DbContext
du.HasIndex(x => x.TotalXp); du.HasIndex(x => x.TotalXp);
du.HasIndex(x => x.CurrencyAmount); du.HasIndex(x => x.CurrencyAmount);
du.HasIndex(x => x.UserId); du.HasIndex(x => x.UserId);
du.HasIndex(x => x.Username);
}); });
#endregion #endregion
@@ -695,7 +702,7 @@ public abstract class NadekoContext : DbContext
gs gs
.Property(x => x.IsEnabled) .Property(x => x.IsEnabled)
.HasDefaultValue(false); .HasDefaultValue(false);
gs gs
.Property(x => x.AutoDeleteTimer) .Property(x => x.AutoDeleteTimer)
.HasDefaultValue(0); .HasDefaultValue(0);

View File

@@ -38,6 +38,7 @@ left join guildconfigs on reactionrolemessage.guildconfigid = guildconfigs.id;")
DELETE FROM "DelMsgOnCmdChannel" WHERE "GuildConfigId" is NULL; DELETE FROM "DelMsgOnCmdChannel" WHERE "GuildConfigId" is NULL;
DELETE FROM "WarningPunishment" WHERE "GuildConfigId" NOT IN (SELECT "Id" from "GuildConfigs"); DELETE FROM "WarningPunishment" WHERE "GuildConfigId" NOT IN (SELECT "Id" from "GuildConfigs");
DELETE FROM "StreamRoleBlacklistedUser" WHERE "StreamRoleSettingsId" is NULL; DELETE FROM "StreamRoleBlacklistedUser" WHERE "StreamRoleSettingsId" is NULL;
DELETE FROM "Permissions" WHERE "GuildConfigId" NOT IN (SELECT "Id" from "GuildConfigs");
"""); """);
} }
@@ -65,4 +66,20 @@ left join guildconfigs on reactionrolemessage.guildconfigid = guildconfigs.id;")
WHERE SendBoostMessage = TRUE; WHERE SendBoostMessage = TRUE;
"""); """);
} }
public static void AddGuildIdsToWarningPunishment(MigrationBuilder builder)
{
builder.Sql("""
DELETE FROM WarningPunishment WHERE GuildConfigId IS NULL OR GuildConfigId NOT IN (SELECT Id FROM GuildConfigs);
UPDATE WarningPunishment
SET GuildId = (SELECT GuildId FROM GuildConfigs WHERE Id = GuildConfigId);
DELETE FROM WarningPunishment as wp
WHERE (wp.Count, wp.GuildConfigId) in (
SELECT wp2.Count, wp2.GuildConfigId FROM WarningPunishment as wp2
GROUP BY wp2.Count, wp2.GuildConfigId
HAVING COUNT(id) > 1
);
""");
}
} }

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

View File

@@ -2938,9 +2938,9 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("timestamp without time zone") .HasColumnType("timestamp without time zone")
.HasColumnName("dateadded"); .HasColumnName("dateadded");
b.Property<int?>("GuildConfigId") b.Property<decimal>("GuildId")
.HasColumnType("integer") .HasColumnType("numeric(20,0)")
.HasColumnName("guildconfigid"); .HasColumnName("guildid");
b.Property<int>("Punishment") b.Property<int>("Punishment")
.HasColumnType("integer") .HasColumnType("integer")
@@ -2957,8 +2957,8 @@ namespace NadekoBot.Migrations.PostgreSql
b.HasKey("Id") b.HasKey("Id")
.HasName("pk_warningpunishment"); .HasName("pk_warningpunishment");
b.HasIndex("GuildConfigId") b.HasAlternateKey("GuildId", "Count")
.HasDatabaseName("ix_warningpunishment_guildconfigid"); .HasName("ak_warningpunishment_guildid_count");
b.ToTable("warningpunishment", (string)null); b.ToTable("warningpunishment", (string)null);
}); });
@@ -3616,15 +3616,6 @@ namespace NadekoBot.Migrations.PostgreSql
b.Navigation("User"); b.Navigation("User");
}); });
modelBuilder.Entity("NadekoBot.Db.Models.WarningPunishment", b =>
{
b.HasOne("NadekoBot.Db.Models.GuildConfig", null)
.WithMany("WarnPunishments")
.HasForeignKey("GuildConfigId")
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("fk_warningpunishment_guildconfigs_guildconfigid");
});
modelBuilder.Entity("NadekoBot.Db.Models.XpCurrencyReward", b => modelBuilder.Entity("NadekoBot.Db.Models.XpCurrencyReward", b =>
{ {
b.HasOne("NadekoBot.Db.Models.XpSettings", "XpSettings") b.HasOne("NadekoBot.Db.Models.XpSettings", "XpSettings")
@@ -3740,8 +3731,6 @@ namespace NadekoBot.Migrations.PostgreSql
b.Navigation("VcRoleInfos"); b.Navigation("VcRoleInfos");
b.Navigation("WarnPunishments");
b.Navigation("XpSettings"); 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);
}
}
}

View File

@@ -2183,7 +2183,7 @@ namespace NadekoBot.Migrations
b.Property<DateTime?>("DateAdded") b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int?>("GuildConfigId") b.Property<ulong>("GuildId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("Punishment") b.Property<int>("Punishment")
@@ -2197,7 +2197,7 @@ namespace NadekoBot.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("GuildConfigId"); b.HasAlternateKey("GuildId", "Count");
b.ToTable("WarningPunishment"); b.ToTable("WarningPunishment");
}); });
@@ -2760,14 +2760,6 @@ namespace NadekoBot.Migrations
b.Navigation("User"); b.Navigation("User");
}); });
modelBuilder.Entity("NadekoBot.Db.Models.WarningPunishment", b =>
{
b.HasOne("NadekoBot.Db.Models.GuildConfig", null)
.WithMany("WarnPunishments")
.HasForeignKey("GuildConfigId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("NadekoBot.Db.Models.XpCurrencyReward", b => modelBuilder.Entity("NadekoBot.Db.Models.XpCurrencyReward", b =>
{ {
b.HasOne("NadekoBot.Db.Models.XpSettings", "XpSettings") b.HasOne("NadekoBot.Db.Models.XpSettings", "XpSettings")
@@ -2880,8 +2872,6 @@ namespace NadekoBot.Migrations
b.Navigation("VcRoleInfos"); b.Navigation("VcRoleInfos");
b.Navigation("WarnPunishments");
b.Navigation("XpSettings"); b.Navigation("XpSettings");
}); });

View File

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

View File

@@ -206,6 +206,18 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, INService
.Where(x => !tempTable.Select(x => x.GuildId) .Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId)) .Contains(x.GuildId))
.DeleteAsync(); .DeleteAsync();
// delete autopublish channels
await ctx.GetTable<AutoPublishChannel>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete greet settings
await ctx.GetTable<GreetSettings>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
return new() return new()
{ {

View File

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

View File

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

View File

@@ -93,6 +93,9 @@ public class GreetService : INService, IReadyExecutor
private Task ClientOnGuildMemberUpdated(Cacheable<SocketGuildUser, ulong> optOldUser, SocketGuildUser newUser) private Task ClientOnGuildMemberUpdated(Cacheable<SocketGuildUser, ulong> optOldUser, SocketGuildUser newUser)
{ {
if (!_enabled[GreetType.Boost].Contains(newUser.Guild.Id))
return Task.CompletedTask;
// if user is a new booster // if user is a new booster
// or boosted again the same server // or boosted again the same server
if ((optOldUser.Value is { PremiumSince: null } && newUser is { PremiumSince: not null }) if ((optOldUser.Value is { PremiumSince: null } && newUser is { PremiumSince: not null })
@@ -134,21 +137,63 @@ public class GreetService : INService, IReadyExecutor
.DeleteAsync(); .DeleteAsync();
} }
private Task OnUserLeft(SocketGuild guild, SocketUser user) private Task OnUserJoined(IGuildUser user)
{ {
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
try
{
if (_enabled[GreetType.Greet].Contains(user.GuildId))
{
var conf = await GetGreetSettingsAsync(user.GuildId, GreetType.Greet);
if (conf?.ChannelId is ulong cid)
{
var channel = await user.Guild.GetTextChannelAsync(cid);
if (channel is not null)
{
await _greetQueue.Writer.WriteAsync((conf, user, channel));
}
}
}
if (_enabled[GreetType.GreetDm].Contains(user.GuildId))
{
var confDm = await GetGreetSettingsAsync(user.GuildId, GreetType.GreetDm);
if (confDm is not null)
{
await _greetQueue.Writer.WriteAsync((confDm, user, null));
}
}
}
catch (Exception ex)
{
Log.Error(ex, "Error in GreetService.OnUserJoined. This should not happen. Please report it");
}
});
return Task.CompletedTask;
}
private Task OnUserLeft(SocketGuild guild, SocketUser user)
{
_ = Task.Run(async () =>
{
if (!_enabled[GreetType.Bye].Contains(guild.Id))
return;
try try
{ {
var conf = await GetGreetSettingsAsync(guild.Id, GreetType.Bye); var conf = await GetGreetSettingsAsync(guild.Id, GreetType.Bye);
if (conf is null) if (conf?.ChannelId is not { } cid)
return; return;
var channel = guild.TextChannels.FirstOrDefault(c => c.Id == conf.ChannelId); var channel = guild.GetChannel(cid) as ITextChannel;
if (channel is null) //maybe warn the server owner that the channel is missing if (channel is null) //maybe warn the server owner that the channel is missing
{ {
Log.Warning("Channel {ChannelId} in {GuildId} was not found. Bye message will be disabled",
conf.ChannelId,
conf.GuildId);
await SetGreet(guild.Id, null, GreetType.Bye, false); await SetGreet(guild.Id, null, GreetType.Bye, false);
return; return;
} }
@@ -163,11 +208,11 @@ public class GreetService : INService, IReadyExecutor
return Task.CompletedTask; return Task.CompletedTask;
} }
private TypedKey<GreetSettings?> GreetSettingsKey(GreetType type) private TypedKey<GreetSettings?> GreetSettingsKey(ulong gid, GreetType type)
=> new($"greet_settings:{type}"); => new($"greet_settings:{gid}:{type}");
public async Task<GreetSettings?> GetGreetSettingsAsync(ulong gid, GreetType type) public async Task<GreetSettings?> GetGreetSettingsAsync(ulong gid, GreetType type)
=> await _cache.GetOrAddAsync<GreetSettings?>(GreetSettingsKey(type), => await _cache.GetOrAddAsync<GreetSettings?>(GreetSettingsKey(gid, type),
() => InternalGetGreetSettingsAsync(gid, type), () => InternalGetGreetSettingsAsync(gid, type),
TimeSpan.FromSeconds(3)); TimeSpan.FromSeconds(3));
@@ -216,9 +261,10 @@ public class GreetService : INService, IReadyExecutor
or DiscordErrorCode.UnknownChannel) or DiscordErrorCode.UnknownChannel)
{ {
Log.Warning(ex, Log.Warning(ex,
"Missing permissions to send a bye message, the greet message will be disabled on server: {GuildId}", "Missing permissions to send a {GreetType} message, it will be disabled on server: {GuildId}",
conf.GreetType,
channel.GuildId); channel.GuildId);
await SetGreet(channel.GuildId, channel.Id, GreetType.Greet, false); await SetGreet(channel.GuildId, channel.Id, conf.GreetType, false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -291,7 +337,7 @@ public class GreetService : INService, IReadyExecutor
await _sender.Response(user).Text(smartText).Sanitize(false).SendAsync(); await _sender.Response(user).Text(smartText).Sanitize(false).SendAsync();
} }
catch(Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Error sending greet dm"); Log.Error(ex, "Error sending greet dm");
return false; return false;
@@ -307,43 +353,6 @@ public class GreetService : INService, IReadyExecutor
IconUrl = user.Guild.IconUrl IconUrl = user.Guild.IconUrl
}; };
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;
}
public static string GetDefaultGreet(GreetType greetType) public static string GetDefaultGreet(GreetType greetType)
=> greetType switch => greetType switch

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
private readonly IBotStrings _strings; private readonly IBotStrings _strings;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private ImmutableDictionary<ulong, IDMChannel> ownerChannels = private ImmutableDictionary<ulong, IDMChannel> ownerChannels =
new Dictionary<ulong, IDMChannel>().ToImmutableDictionary(); new Dictionary<ulong, IDMChannel>().ToImmutableDictionary();
@@ -36,7 +36,7 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
CommandHandler cmdHandler, CommandHandler cmdHandler,
DbService db, DbService db,
IBotStrings strings, IBotStrings strings,
IBotCredentials creds, IBotCreds creds,
IHttpClientFactory factory, IHttpClientFactory factory,
BotConfigService bss, BotConfigService bss,
IPubSub pubSub, IPubSub pubSub,

View File

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

View File

@@ -1,7 +1,6 @@
#nullable disable #nullable disable
using LinqToDB; using LinqToDB;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Common.TypeReaders.Models; using NadekoBot.Common.TypeReaders.Models;
using NadekoBot.Modules.Permissions.Services; using NadekoBot.Modules.Permissions.Services;
@@ -83,17 +82,24 @@ public class UserPunishService : INService, IReadyExecutor
}; };
long previousCount; long previousCount;
List<WarningPunishment> ps; var ps = await WarnPunishList(guildId);
await using (var uow = _db.GetDbContext()) await using (var uow = _db.GetDbContext())
{ {
ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments; previousCount = uow.GetTable<Warning>()
.Where(w => w.GuildId == guildId && w.UserId == userId && !w.Forgiven)
previousCount = uow.Set<Warning>()
.ForId(guildId, userId)
.Where(w => !w.Forgiven && w.UserId == userId)
.Sum(x => x.Weight); .Sum(x => x.Weight);
uow.Set<Warning>().Add(warn); await uow.GetTable<Warning>()
.InsertAsync(() => new()
{
UserId = userId,
GuildId = guildId,
Forgiven = false,
Reason = reason,
Moderator = modName,
Weight = weight,
DateAdded = DateTime.UtcNow,
});
await uow.SaveChangesAsync(); await uow.SaveChangesAsync();
} }
@@ -260,12 +266,12 @@ public class UserPunishService : INService, IReadyExecutor
.ToListAsyncLinqToDB(); .ToListAsyncLinqToDB();
var cleared = await uow.GetTable<Warning>() var cleared = await uow.GetTable<Warning>()
.Where(x => toClear.Contains(x.Id)) .Where(x => toClear.Contains(x.Id))
.UpdateAsync(_ => new() .UpdateAsync(_ => new()
{ {
Forgiven = true, Forgiven = true,
ForgivenBy = "expiry" ForgivenBy = "expiry"
}); });
var toDelete = await uow.GetTable<Warning>() var toDelete = await uow.GetTable<Warning>()
.Where(x => uow.GetTable<GuildConfig>() .Where(x => uow.GetTable<GuildConfig>()
@@ -282,8 +288,8 @@ public class UserPunishService : INService, IReadyExecutor
.ToListAsyncLinqToDB(); .ToListAsyncLinqToDB();
var deleted = await uow.GetTable<Warning>() var deleted = await uow.GetTable<Warning>()
.Where(x => toDelete.Contains(x.Id)) .Where(x => toDelete.Contains(x.Id))
.DeleteAsync(); .DeleteAsync();
if (cleared > 0 || deleted > 0) if (cleared > 0 || deleted > 0)
{ {
@@ -324,11 +330,11 @@ public class UserPunishService : INService, IReadyExecutor
await uow.SaveChangesAsync(); await uow.SaveChangesAsync();
} }
public Task<int> GetWarnExpire(ulong guildId) public Task<(int, bool)> GetWarnExpire(ulong guildId)
{ {
using var uow = _db.GetDbContext(); using var uow = _db.GetDbContext();
var config = uow.GuildConfigsForId(guildId, set => set); var config = uow.GuildConfigsForId(guildId, set => set);
return Task.FromResult(config.WarnExpireHours / 24); return Task.FromResult((config.WarnExpireHours / 24, config.WarnExpireAction == WarnExpireAction.Delete));
} }
public async Task WarnExpireAsync(ulong guildId, int days, bool delete) public async Task WarnExpireAsync(ulong guildId, int days, bool delete)
@@ -377,18 +383,19 @@ public class UserPunishService : INService, IReadyExecutor
return toReturn; return toReturn;
} }
public bool WarnPunish( public async Task<bool> WarnPunish(
ulong guildId, ulong guildId,
int number, int number,
PunishmentAction punish, PunishmentAction punish,
StoopidTime time, TimeSpan? time,
IRole role = null) IRole role = null)
{ {
// these 3 don't make sense with time // these 3 don't make sense with time
if (punish is PunishmentAction.Softban or PunishmentAction.Kick or PunishmentAction.RemoveRoles if (punish is PunishmentAction.Softban or PunishmentAction.Kick or PunishmentAction.RemoveRoles
&& time is not null) && time is not null)
return false; return false;
if (number <= 0 || (time is not null && time.Time > TimeSpan.FromDays(49)))
if (number <= 0 || (time is not null && time > TimeSpan.FromDays(59)))
return false; return false;
if (punish is PunishmentAction.AddRole && role is null) if (punish is PunishmentAction.AddRole && role is null)
@@ -397,47 +404,51 @@ public class UserPunishService : INService, IReadyExecutor
if (punish is PunishmentAction.TimeOut && time is null) if (punish is PunishmentAction.TimeOut && time is null)
return false; return false;
using var uow = _db.GetDbContext(); var timeMinutes = (int?)time?.TotalMinutes ?? 0;
var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments; var roleId = punish == PunishmentAction.AddRole ? role!.Id : default(ulong?);
var toDelete = ps.Where(x => x.Count == number); await using var uow = _db.GetDbContext();
await uow.GetTable<WarningPunishment>()
uow.RemoveRange(toDelete); .InsertOrUpdateAsync(() => new()
{
ps.Add(new() GuildId = guildId,
{ Count = number,
Count = number, Punishment = punish,
Punishment = punish, Time = timeMinutes,
Time = (int?)time?.Time.TotalMinutes ?? 0, RoleId = roleId
RoleId = punish == PunishmentAction.AddRole ? role!.Id : default(ulong?) },
}); _ => new()
uow.SaveChanges(); {
Punishment = punish,
Time = timeMinutes,
RoleId = roleId
},
() => new()
{
GuildId = guildId,
Count = number
});
return true; return true;
} }
public bool WarnPunishRemove(ulong guildId, int number) public async Task<bool> WarnPunishRemove(ulong guildId, int count)
{ {
if (number <= 0) await using var uow = _db.GetDbContext();
return false; var numDeleted = await uow.GetTable<WarningPunishment>()
.DeleteAsync(x => x.GuildId == guildId && x.Count == count);
using var uow = _db.GetDbContext(); return numDeleted > 0;
var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
var p = ps.FirstOrDefault(x => x.Count == number);
if (p is not null)
{
uow.Remove(p);
uow.SaveChanges();
}
return true;
} }
public WarningPunishment[] WarnPunishList(ulong guildId)
public async Task<WarningPunishment[]> WarnPunishList(ulong guildId)
{ {
using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
return uow.GuildConfigsForId(guildId, gc => gc.Include(x => x.WarnPunishments))
.WarnPunishments.OrderBy(x => x.Count) var wps = uow.GetTable<WarningPunishment>()
.ToArray(); .Where(x => x.GuildId == guildId)
.OrderBy(x => x.Count)
.ToArray();
return wps;
} }
public (IReadOnlyCollection<(string Original, ulong? Id, string Reason)> Bans, int Missing) MassKill( public (IReadOnlyCollection<(string Original, ulong? Id, string Reason)> Bans, int Missing) MassKill(
@@ -607,12 +618,12 @@ public class UserPunishService : INService, IReadyExecutor
return await _repSvc.ReplaceAsync(output, repCtx); return await _repSvc.ReplaceAsync(output, repCtx);
} }
public async Task<Warning> WarnDelete(ulong userId, int index) public async Task<Warning> WarnDelete(ulong guildId, ulong userId, int index)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var warn = await uow.GetTable<Warning>() var warn = await uow.GetTable<Warning>()
.Where(x => x.UserId == userId) .Where(x => x.GuildId == guildId && x.UserId == userId)
.OrderByDescending(x => x.DateAdded) .OrderByDescending(x => x.DateAdded)
.Skip(index) .Skip(index)
.FirstOrDefaultAsyncLinqToDB(); .FirstOrDefaultAsyncLinqToDB();
@@ -626,4 +637,73 @@ public class UserPunishService : INService, IReadyExecutor
return warn; return warn;
} }
public async Task<bool> WarnDelete(ulong guildId, int id)
{
await using var uow = _db.GetDbContext();
var numDeleted = await uow.GetTable<Warning>()
.Where(x => x.GuildId == guildId && x.Id == id)
.DeleteAsync();
return numDeleted > 0;
}
public async Task<(IReadOnlyCollection<Warning> latest, int totalCount)> GetLatestWarnings(
ulong guildId,
int page = 1)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(page);
await using var uow = _db.GetDbContext();
var latest = await uow.GetTable<Warning>()
.Where(x => x.GuildId == guildId)
.OrderByDescending(x => x.DateAdded)
.Skip(10 * (page - 1))
.Take(10)
.ToListAsyncLinqToDB();
var totalCount = await uow.GetTable<Warning>()
.Where(x => x.GuildId == guildId)
.CountAsyncLinqToDB();
return (latest, totalCount);
}
public async Task<bool> ForgiveWarning(ulong requestGuildId, int warnId, string modName)
{
await using var uow = _db.GetDbContext();
var success = await uow.GetTable<Warning>()
.Where(x => x.GuildId == requestGuildId && x.Id == warnId)
.UpdateAsync(_ => new()
{
Forgiven = true,
ForgivenBy = modName,
})
== 1;
return success;
}
public async Task<(IReadOnlyCollection<Warning> latest, int totalCount)> GetUserWarnings(
ulong guildId,
ulong userId,
int page)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(page);
await using var uow = _db.GetDbContext();
var latest = await uow.GetTable<Warning>()
.Where(x => x.GuildId == guildId && x.UserId == userId)
.OrderByDescending(x => x.DateAdded)
.Skip(10 * (page - 1))
.Take(10)
.ToListAsyncLinqToDB();
var totalCount = await uow.GetTable<Warning>()
.Where(x => x.GuildId == guildId && x.UserId == userId)
.CountAsyncLinqToDB();
return (latest, totalCount);
}
} }

View File

@@ -6,48 +6,7 @@ namespace NadekoBot.Modules.NadekoExpressions;
public static class NadekoExpressionExtensions public static class NadekoExpressionExtensions
{ {
private static string ResolveTriggerString(this string str, DiscordSocketClient client)
=> str.Replace("%bot.mention%", client.CurrentUser.Mention, StringComparison.Ordinal);
public static async Task<IUserMessage> Send(
this NadekoExpression cr,
IUserMessage ctx,
IReplacementService repSvc,
DiscordSocketClient client,
IMessageSenderService sender)
{
var channel = cr.DmResponse ? await ctx.Author.CreateDMChannelAsync() : ctx.Channel;
var trigger = cr.Trigger.ResolveTriggerString(client);
var substringIndex = trigger.Length;
if (cr.ContainsAnywhere)
{
var pos = ctx.Content.AsSpan().GetWordPosition(trigger);
if (pos == WordPosition.Start)
substringIndex += 1;
else if (pos == WordPosition.End)
substringIndex = ctx.Content.Length;
else if (pos == WordPosition.Middle)
substringIndex += ctx.Content.IndexOf(trigger, StringComparison.InvariantCulture);
}
var canMentionEveryone = (ctx.Author as IGuildUser)?.GuildPermissions.MentionEveryone ?? true;
var repCtx = new ReplacementContext(client: client,
guild: (ctx.Channel as ITextChannel)?.Guild as SocketGuild,
channel: ctx.Channel,
user: ctx.Author
)
.WithOverride("%target%",
() => canMentionEveryone
? ctx.Content[substringIndex..].Trim()
: ctx.Content[substringIndex..].Trim().SanitizeMentions(true));
var text = SmartText.CreateFrom(cr.Response);
text = await repSvc.ReplaceAsync(text, repCtx);
return await sender.Response(channel).Text(text).Sanitize(false).SendAsync();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static WordPosition GetWordPosition(this ReadOnlySpan<char> str, in ReadOnlySpan<char> word) public static WordPosition GetWordPosition(this ReadOnlySpan<char> str, in ReadOnlySpan<char> word)

View File

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

View File

@@ -67,7 +67,6 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
// private readonly GlobalPermissionService _gperm; // private readonly GlobalPermissionService _gperm;
// private readonly CmdCdService _cmdCds; // private readonly CmdCdService _cmdCds;
private readonly IPermissionChecker _permChecker; private readonly IPermissionChecker _permChecker;
private readonly ICommandHandler _cmd;
private readonly IBotStrings _strings; private readonly IBotStrings _strings;
private readonly IBot _bot; private readonly IBot _bot;
private readonly IPubSub _pubSub; private readonly IPubSub _pubSub;
@@ -84,7 +83,6 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
IBotStrings strings, IBotStrings strings,
IBot bot, IBot bot,
DiscordSocketClient client, DiscordSocketClient client,
ICommandHandler cmd,
IPubSub pubSub, IPubSub pubSub,
IMessageSenderService sender, IMessageSenderService sender,
IReplacementService repSvc, IReplacementService repSvc,
@@ -93,7 +91,6 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
{ {
_db = db; _db = db;
_client = client; _client = client;
_cmd = cmd;
_strings = strings; _strings = strings;
_bot = bot; _bot = bot;
_pubSub = pubSub; _pubSub = pubSub;
@@ -249,46 +246,54 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
try try
{ {
if (guild is SocketGuild sg) if (guild is not SocketGuild sg)
return false;
var result = await _permChecker.CheckPermsAsync(
guild,
msg.Channel,
msg.Author,
"ACTUALEXPRESSIONS",
expr.Trigger
);
if (!result.IsAllowed)
{ {
var result = await _permChecker.CheckPermsAsync( var cache = _pc.GetCacheFor(guild.Id);
guild, if (cache.Verbose)
msg.Channel,
msg.Author,
"ACTUALEXPRESSIONS",
expr.Trigger
);
if (!result.IsAllowed)
{ {
var cache = _pc.GetCacheFor(guild.Id); if (result.TryPickT3(out var disallowed, out _))
if (cache.Verbose)
{ {
if (result.TryPickT3(out var disallowed, out _)) var permissionMessage = _strings.GetText(strs.perm_prevent(disallowed.PermIndex + 1,
Format.Bold(disallowed.PermText)),
sg.Id);
try
{
await _sender.Response(msg.Channel)
.Error(permissionMessage)
.SendAsync();
}
catch
{ {
var permissionMessage = _strings.GetText(strs.perm_prevent(disallowed.PermIndex + 1,
Format.Bold(disallowed.PermText)),
sg.Id);
try
{
await _sender.Response(msg.Channel)
.Error(permissionMessage)
.SendAsync();
}
catch
{
}
Log.Information("{PermissionMessage}", permissionMessage);
} }
}
return true; Log.Information("{PermissionMessage}", permissionMessage);
}
} }
return true;
} }
var sentMsg = await expr.Send(msg, _repSvc, _client, _sender); var cu = sg.CurrentUser;
var channel = expr.DmResponse ? await msg.Author.CreateDMChannelAsync() : msg.Channel;
// have no perms to speak in that channel
if (channel is ITextChannel tc && !cu.GetPermissions(tc).SendMessages)
return false;
var sentMsg = await Send(expr, msg, channel);
var reactions = expr.GetReactions(); var reactions = expr.GetReactions();
foreach (var reaction in reactions) foreach (var reaction in reactions)
@@ -336,6 +341,47 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
return false; return false;
} }
public string ResolveTriggerString(string str)
=> str.Replace("%bot.mention%", _client.CurrentUser.Mention, StringComparison.Ordinal);
public async Task<IUserMessage> Send(
NadekoExpression cr,
IUserMessage ctx,
IMessageChannel channel
)
{
var trigger = ResolveTriggerString(cr.Trigger);
var substringIndex = trigger.Length;
if (cr.ContainsAnywhere)
{
var pos = ctx.Content.AsSpan().GetWordPosition(trigger);
if (pos == WordPosition.Start)
substringIndex += 1;
else if (pos == WordPosition.End)
substringIndex = ctx.Content.Length;
else if (pos == WordPosition.Middle)
substringIndex += ctx.Content.IndexOf(trigger, StringComparison.InvariantCulture);
}
var canMentionEveryone = (ctx.Author as IGuildUser)?.GuildPermissions.MentionEveryone ?? true;
var repCtx = new ReplacementContext(client: _client,
guild: (ctx.Channel as ITextChannel)?.Guild as SocketGuild,
channel: ctx.Channel,
user: ctx.Author
)
.WithOverride("%target%",
() => canMentionEveryone
? ctx.Content[substringIndex..].Trim()
: ctx.Content[substringIndex..].Trim().SanitizeMentions(true));
var text = SmartText.CreateFrom(cr.Response);
text = await _repSvc.ReplaceAsync(text, repCtx);
return await _sender.Response(channel).Text(text).Sanitize(false).SendAsync();
}
public async Task ResetExprReactions(ulong? maybeGuildId, int id) public async Task ResetExprReactions(ulong? maybeGuildId, int id)
{ {
NadekoExpression expr; NadekoExpression expr;
@@ -789,7 +835,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
if (newguildExpressions.TryGetValue(guildId, out var exprs)) if (newguildExpressions.TryGetValue(guildId, out var exprs))
{ {
return (exprs.Where(x => x.Trigger.Contains(query)) return (exprs.Where(x => x.Trigger.Contains(query) || x.Response.Contains(query))
.Skip(page * 9) .Skip(page * 9)
.Take(9) .Take(9)
.ToArray(), exprs.Length); .ToArray(), exprs.Length);

View File

@@ -1,6 +1,7 @@
#nullable disable #nullable disable
using LinqToDB; using LinqToDB;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using LinqToDB.Tools;
using NadekoBot.Db.Models; using NadekoBot.Db.Models;
using NadekoBot.Modules.Gambling.Bank; using NadekoBot.Modules.Gambling.Bank;
using NadekoBot.Modules.Gambling.Common; using NadekoBot.Modules.Gambling.Common;
@@ -127,7 +128,7 @@ public partial class Gambling : GamblingModule<GamblingService>
customId: "timely:remind_me"), customId: "timely:remind_me"),
(smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromHours(period))) (smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromHours(period)))
); );
// Creates timely reminder button, parameter in milliseconds. // Creates timely reminder button, parameter in milliseconds.
private NadekoInteractionBase CreateRemindMeInteraction(double ms) private NadekoInteractionBase CreateRemindMeInteraction(double ms)
=> _inter => _inter
@@ -166,7 +167,7 @@ public partial class Gambling : GamblingModule<GamblingService>
await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(interaction).SendAsync(); await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(interaction).SendAsync();
return; return;
} }
var patron = await _ps.GetPatronAsync(ctx.User.Id); var patron = await _ps.GetPatronAsync(ctx.User.Id);
@@ -625,8 +626,6 @@ public partial class Gambling : GamblingModule<GamblingService>
var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args); var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
// List<DiscordUser> cleanRichest;
// it's pointless to have clean on dm context
if (ctx.Guild is null) if (ctx.Guild is null)
{ {
opts.Clean = false; opts.Clean = false;
@@ -640,13 +639,18 @@ public partial class Gambling : GamblingModule<GamblingService>
await ctx.Channel.TriggerTypingAsync(); await ctx.Channel.TriggerTypingAsync();
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild); await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
var users = ((SocketGuild)ctx.Guild).Users.Map(x => x.Id);
var perPage = 9;
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var cleanRichest = await uow.GetTable<DiscordUser>()
.Where(x => x.UserId.In(users))
.OrderByDescending(x => x.CurrencyAmount)
.Skip(curPage * perPage)
.Take(perPage)
.ToListAsync();
var cleanRichest = await uow.Set<DiscordUser>() return cleanRichest;
.GetTopRichest(_client.CurrentUser.Id, 0, 1000);
var sg = (SocketGuild)ctx.Guild!;
return cleanRichest.Where(x => sg.GetUser(x.UserId) is not null).ToList();
} }
else else
{ {
@@ -655,13 +659,9 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
} }
var res = Response()
.Paginated();
await Response() await Response()
.Paginated() .Paginated()
.PageItems(GetTopRichest) .PageItems(GetTopRichest)
.TotalElements(900)
.PageSize(9) .PageSize(9)
.CurrentPage(page) .CurrentPage(page)
.Page((toSend, curPage) => .Page((toSend, curPage) =>

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ public class WaifuService : INService, IReadyExecutor
private readonly ICurrencyService _cs; private readonly ICurrencyService _cs;
private readonly IBotCache _cache; private readonly IBotCache _cache;
private readonly GamblingConfigService _gss; private readonly GamblingConfigService _gss;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
public WaifuService( public WaifuService(
@@ -23,7 +23,7 @@ public class WaifuService : INService, IReadyExecutor
ICurrencyService cs, ICurrencyService cs,
IBotCache cache, IBotCache cache,
GamblingConfigService gss, GamblingConfigService gss,
IBotCredentials creds, IBotCreds creds,
DiscordSocketClient client) DiscordSocketClient client)
{ {
_db = db; _db = db;
@@ -300,10 +300,10 @@ public class WaifuService : INService, IReadyExecutor
return (oldAff, success, remaining); return (oldAff, success, remaining);
} }
public IEnumerable<WaifuLbResult> GetTopWaifusAtPage(int page, int perPage = 9) public async Task<IReadOnlyList<WaifuLbResult>> GetTopWaifusAtPage(int page, int perPage = 9)
{ {
using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
return uow.Set<WaifuInfo>().GetTop(perPage, page * perPage); return await uow.Set<WaifuInfo>().GetTop(perPage, page * perPage);
} }
public ulong GetWaifuUserId(ulong ownerId, string name) public ulong GetWaifuUserId(ulong ownerId, string name)

View File

@@ -25,30 +25,35 @@ public static class WaifuExtensions
return includes(waifus).AsQueryable().FirstOrDefault(wi => wi.Waifu.UserId == userId); return includes(waifus).AsQueryable().FirstOrDefault(wi => wi.Waifu.UserId == userId);
} }
public static IEnumerable<WaifuLbResult> GetTop(this DbSet<WaifuInfo> waifus, int count, int skip = 0) public static async Task<IReadOnlyList<WaifuLbResult>> GetTop(this DbSet<WaifuInfo> waifus, int count, int skip = 0)
{ {
ArgumentOutOfRangeException.ThrowIfNegative(count); ArgumentOutOfRangeException.ThrowIfNegative(count);
if (count == 0) if (count == 0)
return []; return [];
return waifus.Include(wi => wi.Waifu) return await waifus.Include(wi => wi.Waifu)
.Include(wi => wi.Affinity) .Include(wi => wi.Affinity)
.Include(wi => wi.Claimer) .Include(wi => wi.Claimer)
.OrderByDescending(wi => wi.Price) .OrderByDescending(wi => wi.Price)
.Skip(skip) .Skip(skip)
.Take(count) .Take(count)
.Select(x => new WaifuLbResult .Select(x => new WaifuLbResult
{ {
Affinity = x.Affinity == null ? null : x.Affinity.Username, Affinity = x.Affinity == null
AffinityDiscrim = x.Affinity == null ? null : x.Affinity.Discriminator, ? null
Claimer = x.Claimer == null ? null : x.Claimer.Username, : x.Affinity.Username
ClaimerDiscrim = x.Claimer == null ? null : x.Claimer.Discriminator, + (x.Affinity.Discriminator != "0000" ? "#" + x.Affinity.Discriminator : ""),
Username = x.Waifu.Username, ClaimerName =
Discrim = x.Waifu.Discriminator, x.Claimer == null
Price = x.Price ? null
}) : x.Claimer.Username
.ToList(); + (x.Claimer.Discriminator != "0000" ? "#" + x.Claimer.Discriminator : ""),
WaifuName = x.Waifu.Username
+ (x.Waifu.Discriminator != "0000" ? "#" + x.Waifu.Discriminator : ""),
Price = x.Price
})
.ToListAsyncEF();
} }
public static decimal GetTotalValue(this DbSet<WaifuInfo> waifus) public static decimal GetTotalValue(this DbSet<WaifuInfo> waifus)
@@ -64,7 +69,7 @@ public static class WaifuExtensions
public static async Task<WaifuInfoStats> GetWaifuInfoAsync(this DbContext ctx, ulong userId) public static async Task<WaifuInfoStats> GetWaifuInfoAsync(this DbContext ctx, ulong userId)
{ {
await ctx.EnsureUserCreatedAsync(userId); await ctx.EnsureUserCreatedAsync(userId);
await ctx.Set<WaifuInfo>() await ctx.Set<WaifuInfo>()
.ToLinqToDBTable() .ToLinqToDBTable()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()

View File

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

View File

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

View File

@@ -12,9 +12,9 @@ public sealed partial class Music
{ {
private static readonly SemaphoreSlim _playlistLock = new(1, 1); private static readonly SemaphoreSlim _playlistLock = new(1, 1);
private readonly DbService _db; private readonly DbService _db;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
public PlaylistCommands(DbService db, IBotCredentials creds) public PlaylistCommands(DbService db, IBotCreds creds)
{ {
_db = db; _db = db;
_creds = creds; _creds = creds;

View File

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

View File

@@ -16,11 +16,11 @@ public class CryptoService : INService
{ {
private readonly IBotCache _cache; private readonly IBotCache _cache;
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly SemaphoreSlim _getCryptoLock = new(1, 1); private readonly SemaphoreSlim _getCryptoLock = new(1, 1);
public CryptoService(IBotCache cache, IHttpClientFactory httpFactory, IBotCredentials creds) public CryptoService(IBotCache cache, IHttpClientFactory httpFactory, IBotCreds creds)
{ {
_cache = cache; _cache = cache;
_httpFactory = httpFactory; _httpFactory = httpFactory;

View File

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

View File

@@ -7,9 +7,9 @@ namespace NadekoBot.Modules.Searches;
public sealed class OsuService : INService public sealed class OsuService : INService
{ {
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
public OsuService(IHttpClientFactory httpFactory, IBotCredentials creds) public OsuService(IHttpClientFactory httpFactory, IBotCreds creds)
{ {
_httpFactory = httpFactory; _httpFactory = httpFactory;
_creds = creds; _creds = creds;

View File

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

View File

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

View File

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

View File

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

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> public partial class Searches : NadekoModule<SearchesService>
{ {
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly IGoogleApiService _google; private readonly IGoogleApiService _google;
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly IMemoryCache _cache; private readonly IMemoryCache _cache;
private readonly ITimezoneService _tzSvc; private readonly ITimezoneService _tzSvc;
public Searches( public Searches(
IBotCredentials creds, IBotCreds creds,
IGoogleApiService google, IGoogleApiService google,
IHttpClientFactory factory, IHttpClientFactory factory,
IMemoryCache cache, IMemoryCache cache,

View File

@@ -28,11 +28,9 @@ public partial class SearchesConfig : ICloneable<SearchesConfig>
[Comment(""" [Comment("""
Which search provider will be used for the `.youtube` and `.q` commands. Which search provider will be used for the `.youtube` and `.q` commands.
- `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console - `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console. `.q` is not supported for this setting. It will fallback to yt-dlp.
- `ytdl` - default, uses youtube-dl. Requires `youtube-dl` to be installed and it's path added to env variables. Slow. - `ytdlp` - default, recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables
- `ytdlp` - recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables
- `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property - `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property
""")] """)]
@@ -77,9 +75,9 @@ public sealed class FollowedStreamConfig
public enum YoutubeSearcher public enum YoutubeSearcher
{ {
YtDataApiv3, YtDataApiv3 = 0,
Ytdl, Ytdl = 1,
Ytdlp, Ytdlp = 1,
Invid, Invid = 3,
Invidious = 3 Invidious = 3
} }

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ public sealed class GiveawayService : INService, IReadyExecutor
public static string GiveawayEmoji = "🎉"; public static string GiveawayEmoji = "🎉";
private readonly DbService _db; private readonly DbService _db;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly IMessageSenderService _sender; private readonly IMessageSenderService _sender;
private readonly IBotStrings _strings; private readonly IBotStrings _strings;
@@ -20,7 +20,7 @@ public sealed class GiveawayService : INService, IReadyExecutor
private SortedSet<GiveawayModel> _giveawayCache = new SortedSet<GiveawayModel>(); private SortedSet<GiveawayModel> _giveawayCache = new SortedSet<GiveawayModel>();
private readonly NadekoRandom _rng; private readonly NadekoRandom _rng;
public GiveawayService(DbService db, IBotCredentials creds, DiscordSocketClient client, public GiveawayService(DbService db, IBotCreds creds, DiscordSocketClient client,
IMessageSenderService sender, IBotStrings strings, ILocalization localization, IMemoryCache cache) IMessageSenderService sender, IBotStrings strings, ILocalization localization, IMemoryCache cache)
{ {
_db = db; _db = db;

View File

@@ -26,6 +26,8 @@ public interface IQuoteService
ulong guildId, ulong guildId,
string? keyword, string? keyword,
string text); string text);
Task<(IReadOnlyCollection<Quote> quotes, int totalCount)> FindQuotesAsync(ulong guildId, string query, int page);
Task<IReadOnlyCollection<Quote>> GetGuildQuotesAsync(ulong guildId); Task<IReadOnlyCollection<Quote>> GetGuildQuotesAsync(ulong guildId);
Task<int> RemoveAllByKeyword(ulong guildId, string keyword); Task<int> RemoveAllByKeyword(ulong guildId, string keyword);
@@ -39,6 +41,7 @@ public interface IQuoteService
string text); string text);
Task<Quote?> EditQuoteAsync(ulong authorId, int quoteId, string text); Task<Quote?> EditQuoteAsync(ulong authorId, int quoteId, string text);
Task<Quote?> EditQuoteAsync(ulong guildId, int quoteId, string keyword, string text);
Task<bool> DeleteQuoteAsync( Task<bool> DeleteQuoteAsync(
ulong guildId, ulong guildId,

View File

@@ -169,6 +169,23 @@ public sealed class QuoteService : IQuoteService, INService
return q; return q;
} }
public async Task<Quote?> EditQuoteAsync(
ulong guildId,
int quoteId,
string keyword,
string text)
{
await using var uow = _db.GetDbContext();
var result = await uow.GetTable<Quote>()
.Where(x => x.Id == quoteId && x.GuildId == guildId)
.Set(x => x.Keyword, keyword)
.Set(x => x.Text, text)
.UpdateWithOutputAsync((del, ins) => ins);
var q = result.FirstOrDefault();
return q;
}
public async Task<bool> DeleteQuoteAsync( public async Task<bool> DeleteQuoteAsync(
ulong guildId, ulong guildId,
ulong authorId, ulong authorId,
@@ -219,4 +236,24 @@ public sealed class QuoteService : IQuoteService, INService
return true; return true;
} }
public async Task<(IReadOnlyCollection<Quote> quotes, int totalCount)> FindQuotesAsync(
ulong guildId,
string query,
int page)
{
await using var uow = _db.GetDbContext();
var baseQuery = uow.GetTable<Quote>()
.Where(x => x.GuildId == guildId)
.Where(x => x.Keyword.Contains(query) || x.Text.Contains(query));
var quotes = await baseQuery
.OrderBy(x => x.Id)
.Skip((page - 1) * 10)
.Take(10)
.ToListAsyncLinqToDB();
return (quotes, await baseQuery.CountAsyncLinqToDB());
}
} }

View File

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

View File

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

View File

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

View File

@@ -107,7 +107,7 @@ public partial class Xp : NadekoModule<XpService>
[Cmd] [Cmd]
[UserPerm(GuildPerm.ManageChannels)] [UserPerm(GuildPerm.ManageChannels)]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task XpExclude(Channel _, [Leftover] IChannel channel = null) public async Task XpExclude(Channel _, [Leftover] IChannel? channel = null)
{ {
if (channel is null) if (channel is null)
channel = ctx.Channel; channel = ctx.Channel;
@@ -182,29 +182,28 @@ public partial class Xp : NadekoModule<XpService>
var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args); var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
await ctx.Channel.TriggerTypingAsync(); await ctx.Channel.TriggerTypingAsync();
var socketGuild = (SocketGuild)ctx.Guild;
var allCleanUsers = new List<UserXpStats>();
if (opts.Clean) if (opts.Clean)
{ {
await ctx.Channel.TriggerTypingAsync();
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild); await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
allCleanUsers = (await _service.GetTopUserXps(ctx.Guild.Id, 1000))
.Where(user => socketGuild.GetUser(user.UserId) is not null)
.ToList();
} }
var res = opts.Clean async Task<IReadOnlyCollection<UserXpStats>> GetPageItems(int curPage)
? Response() {
.Paginated() var socketGuild = (SocketGuild)ctx.Guild;
.Items(allCleanUsers) if (opts.Clean)
: Response() {
.Paginated() return await _service.GetGuildUserXps(ctx.Guild.Id,
.PageItems((curPage) => _service.GetUserXps(ctx.Guild.Id, curPage)); socketGuild.Users.Select(x => x.Id).ToList(),
curPage);
}
await res return await _service.GetGuildUserXps(ctx.Guild.Id, curPage);
.PageSize(9) }
await Response()
.Paginated()
.PageItems(GetPageItems)
.PageSize(10)
.CurrentPage(page) .CurrentPage(page)
.Page((users, curPage) => .Page((users, curPage) =>
{ {
@@ -237,15 +236,33 @@ public partial class Xp : NadekoModule<XpService>
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task XpGlobalLeaderboard(int page = 1) public async Task XpGlobalLeaderboard(int page = 1, params string[] args)
{ {
if (--page < 0 || page > 99) if (--page < 0 || page > 99)
return; return;
var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
await ctx.Channel.TriggerTypingAsync();
if (opts.Clean)
{
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
}
async Task<IReadOnlyCollection<DiscordUser>> GetPageItems(int curPage)
{
if (opts.Clean)
{
return await _service.GetGlobalUserXps(page, ((SocketGuild)ctx.Guild).Users.Select(x => x.Id).ToList());
}
return await _service.GetGlobalUserXps(curPage);
}
await Response() await Response()
.Paginated() .Paginated()
.PageItems(async curPage => await _service.GetUserXps(curPage)) .PageItems(GetPageItems)
.PageSize(9) .PageSize(10)
.Page((users, curPage) => .Page((users, curPage) =>
{ {
var embed = _sender.CreateEmbed() var embed = _sender.CreateEmbed()
@@ -282,7 +299,9 @@ public partial class Xp : NadekoModule<XpService>
if (role.IsManaged) if (role.IsManaged)
return; return;
var count = await _service.AddXpToUsersAsync(ctx.Guild.Id, amount, role.Members.Select(x => x.Id).ToArray()); var count = await _service.AddXpToUsersAsync(ctx.Guild.Id,
amount,
role.Members.Select(x => x.Id).ToArray());
await Response() await Response()
.Confirm( .Confirm(
strs.xpadd_users(Format.Bold(amount.ToString()), Format.Bold(count.ToString()))) strs.xpadd_users(Format.Bold(amount.ToString()), Format.Bold(count.ToString())))

View File

@@ -12,6 +12,7 @@ using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using System.Threading.Channels; using System.Threading.Channels;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using LinqToDB.Tools;
using NadekoBot.Modules.Patronage; using NadekoBot.Modules.Patronage;
using Color = SixLabors.ImageSharp.Color; using Color = SixLabors.ImageSharp.Color;
using Exception = System.Exception; using Exception = System.Exception;
@@ -25,7 +26,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
private readonly IImageCache _images; private readonly IImageCache _images;
private readonly IBotStrings _strings; private readonly IBotStrings _strings;
private readonly FontProvider _fonts; private readonly FontProvider _fonts;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly ICurrencyService _cs; private readonly ICurrencyService _cs;
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly XpConfigService _xpConfig; private readonly XpConfigService _xpConfig;
@@ -55,7 +56,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
IImageCache images, IImageCache images,
IBotCache c, IBotCache c,
FontProvider fonts, FontProvider fonts,
IBotCredentials creds, IBotCreds creds,
ICurrencyService cs, ICurrencyService cs,
IHttpClientFactory http, IHttpClientFactory http,
XpConfigService xpConfig, XpConfigService xpConfig,
@@ -563,23 +564,50 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
uow.SaveChanges(); uow.SaveChanges();
} }
public async Task<IReadOnlyCollection<UserXpStats>> GetUserXps(ulong guildId, int page) public async Task<IReadOnlyCollection<UserXpStats>> GetGuildUserXps(ulong guildId, int page)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
return await uow.Set<UserXpStats>().GetUsersFor(guildId, page); return await uow
.UserXpStats
.Where(x => x.GuildId == guildId)
.OrderByDescending(x => x.Xp + x.AwardedXp)
.Skip(page * 10)
.Take(10)
.ToArrayAsyncLinqToDB();
} }
public async Task<IReadOnlyCollection<UserXpStats>> GetTopUserXps(ulong guildId, int count) public async Task<IReadOnlyCollection<UserXpStats>> GetGuildUserXps(ulong guildId, List<ulong> users, int page)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
return await uow.Set<UserXpStats>().GetTopUserXps(guildId, count); return await uow.Set<UserXpStats>()
.Where(x => x.GuildId == guildId && x.UserId.In(users))
.OrderByDescending(x => x.Xp + x.AwardedXp)
.Skip(page * 10)
.Take(10)
.ToArrayAsyncLinqToDB();
} }
public Task<IReadOnlyCollection<DiscordUser>> GetUserXps(int page, int perPage = 9) public async Task<IReadOnlyCollection<DiscordUser>> GetGlobalUserXps(int page)
{ {
using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
return uow.Set<DiscordUser>()
.GetUsersXpLeaderboardFor(page, perPage); return await uow.GetTable<DiscordUser>()
.OrderByDescending(x => x.TotalXp)
.Skip(page * 10)
.Take(10)
.ToArrayAsyncLinqToDB();
}
public async Task<IReadOnlyCollection<DiscordUser>> GetGlobalUserXps(int page, List<ulong> users)
{
await using var uow = _db.GetDbContext();
return await uow.GetTable<DiscordUser>()
.Where(x => x.UserId.In(users))
.OrderByDescending(x => x.TotalXp)
.Skip(page * 10)
.Take(10)
.ToArrayAsyncLinqToDB();
} }
public async Task ChangeNotificationType(ulong userId, ulong guildId, XpNotificationLocation type) public async Task ChangeNotificationType(ulong userId, ulong guildId, XpNotificationLocation type)

View File

@@ -4,7 +4,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.1.10</Version> <Version>5.1.15</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
@@ -34,13 +34,12 @@
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138"/> <PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138"/>
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414"/> <PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414"/>
<PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084"/> <PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084"/>
<!-- <PackageReference Include="Grpc.AspNetCore" Version="2.62.0" />-->
<PackageReference Include="Google.Protobuf" Version="3.26.1"/> <PackageReference Include="Google.Protobuf" Version="3.28.2" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.62.0"/> <PackageReference Include="Grpc" Version="2.46.6" />
<PackageReference Include="Grpc.Tools" Version="2.63.0"> <PackageReference Include="Grpc.Net.Client" Version="2.62.0" />
<PrivateAssets>all</PrivateAssets> <PackageReference Include="Grpc.Tools" Version="2.66.0" PrivateAssets="All" />
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.5.0"/> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.5.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
@@ -69,19 +68,19 @@
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/> <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/>
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1"/> <PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1"/>
<PackageReference Include="SixLabors.Fonts" Version="2.0.4" /> <PackageReference Include="SixLabors.Fonts" Version="2.0.4"/>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.5"/>
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4" /> <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4"/>
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009"/> <PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009"/>
<PackageReference Include="StackExchange.Redis" Version="2.8.0" /> <PackageReference Include="StackExchange.Redis" Version="2.8.0"/>
<PackageReference Include="YamlDotNet" Version="15.1.4"/> <PackageReference Include="YamlDotNet" Version="15.1.4"/>
<PackageReference Include="SharpToken" Version="2.0.3" /> <PackageReference Include="SharpToken" Version="2.0.3"/>
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0"/> <PackageReference Include="JetBrains.Annotations" Version="2023.3.0"/>
<!-- Db-related packages --> <!-- Db-related packages -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -89,7 +88,7 @@
<PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0"/> <PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/>
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/> <PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/>
@@ -103,20 +102,17 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\NadekoBot.GrpcApiBase\NadekoBot.GrpcApiBase.csproj"/>
<ProjectReference Include="..\Nadeko.Medusa\Nadeko.Medusa.csproj"/> <ProjectReference Include="..\Nadeko.Medusa\Nadeko.Medusa.csproj"/>
<ProjectReference Include="..\NadekoBot.Voice\NadekoBot.Voice.csproj"/> <ProjectReference Include="..\NadekoBot.Voice\NadekoBot.Voice.csproj"/>
<ProjectReference Include="..\NadekoBot.Generators\NadekoBot.Generators.csproj" OutputItemType="Analyzer"/> <ProjectReference Include="..\NadekoBot.Generators\NadekoBot.Generators.csproj" OutputItemType="Analyzer"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="data\strings\responses\responses.en-US.json"/> <AdditionalFiles Include="data\strings\responses\responses.en-US.json"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Protobuf Include="..\NadekoBot.Coordinator\Protos\coordinator.proto" GrpcServices="Client">
<Link>Protos\coordinator.proto</Link>
</Protobuf>
<None Update="data\**\*"> <None Update="data\**\*">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile> <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -132,7 +128,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Grpc\" /> <Protobuf Include="..\NadekoBot.Coordinator\Protos\coordinator.proto">
<Link>_common\CoordinatorProtos\coordinator.proto</Link>
<!-- <GrpcServices>Client</GrpcServices>-->
</Protobuf>
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'GlobalNadeko' "> <PropertyGroup Condition=" '$(Configuration)' == 'GlobalNadeko' ">

View File

@@ -1,5 +1,3 @@
var pid = Environment.ProcessId;
var shardId = 0; var shardId = 0;
int? totalShards = null; // 0 to read from creds.yml int? totalShards = null; // 0 to read from creds.yml
if (args.Length > 0 && args[0] != "run") if (args.Length > 0 && args[0] != "run")
@@ -22,7 +20,5 @@ if (args.Length > 0 && args[0] != "run")
} }
} }
LogSetup.SetupLogger(shardId);
Log.Information("Pid: {ProcessId}", pid);
await new Bot(shardId, totalShards, Environment.GetEnvironmentVariable("NadekoBot__creds")).RunAndBlockAsync(); await new Bot(shardId, totalShards, Environment.GetEnvironmentVariable("NadekoBot__creds")).RunAndBlockAsync();

View File

@@ -0,0 +1,152 @@
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using NadekoBot.Db.Models;
using NadekoBot.Modules.NadekoExpressions;
using NadekoBot.Modules.Utility;
namespace NadekoBot.GrpcApi;
public class ExprsSvc : GrpcExprs.GrpcExprsBase, IGrpcSvc, INService
{
private readonly NadekoExpressionsService _svc;
private readonly IQuoteService _qs;
private readonly DiscordSocketClient _client;
public ExprsSvc(NadekoExpressionsService svc, IQuoteService qs, DiscordSocketClient client)
{
_svc = svc;
_qs = qs;
_client = client;
}
public ServerServiceDefinition Bind()
=> GrpcExprs.BindService(this);
private ulong GetUserId(Metadata meta)
=> ulong.Parse(meta.FirstOrDefault(x => x.Key == "userid")!.Value);
public override async Task<AddExprReply> AddExpr(AddExprRequest request, ServerCallContext context)
{
if (string.IsNullOrWhiteSpace(request.Expr.Trigger) || string.IsNullOrWhiteSpace(request.Expr.Response))
throw new RpcException(new Status(StatusCode.InvalidArgument, "Trigger and response are required"));
NadekoExpression expr;
if (!string.IsNullOrWhiteSpace(request.Expr.Id))
{
expr = await _svc.EditAsync(request.GuildId,
new kwum(request.Expr.Id),
request.Expr.Response,
request.Expr.Ca,
request.Expr.Ad,
request.Expr.Dm);
}
else
{
expr = await _svc.AddAsync(request.GuildId,
request.Expr.Trigger,
request.Expr.Response,
request.Expr.Ca,
request.Expr.Ad,
request.Expr.Dm);
}
return new AddExprReply()
{
Id = new kwum(expr.Id).ToString(),
Success = true,
};
}
public override async Task<GetExprsReply> GetExprs(GetExprsRequest request, ServerCallContext context)
{
var (exprs, totalCount) = await _svc.FindExpressionsAsync(request.GuildId, request.Query, request.Page);
var reply = new GetExprsReply();
reply.TotalCount = totalCount;
reply.Expressions.AddRange(exprs.Select(x => new ExprDto()
{
Ad = x.AutoDeleteTrigger,
At = x.AllowTarget,
Ca = x.ContainsAnywhere,
Dm = x.DmResponse,
Response = x.Response,
Id = new kwum(x.Id).ToString(),
Trigger = x.Trigger,
}));
return reply;
}
public override async Task<Empty> DeleteExpr(DeleteExprRequest request, ServerCallContext context)
{
if (kwum.TryParse(request.Id, out var id))
await _svc.DeleteAsync(request.GuildId, id);
return new Empty();
}
public override async Task<GetQuotesReply> GetQuotes(GetQuotesRequest request, ServerCallContext context)
{
if (request.Page < 0)
throw new RpcException(new Status(StatusCode.InvalidArgument, "Page must be >= 0"));
var (quotes, totalCount) = await _qs.FindQuotesAsync(request.GuildId, request.Query, request.Page);
var reply = new GetQuotesReply();
reply.TotalCount = totalCount;
reply.Quotes.AddRange(quotes.Select(x => new QuoteDto()
{
Id = new kwum(x.Id).ToString(),
Trigger = x.Keyword,
Response = x.Text,
AuthorId = x.AuthorId,
AuthorName = x.AuthorName
}));
return reply;
}
public override async Task<AddQuoteReply> AddQuote(AddQuoteRequest request, ServerCallContext context)
{
var userId = GetUserId(context.RequestHeaders);
if (string.IsNullOrWhiteSpace(request.Quote.Trigger) || string.IsNullOrWhiteSpace(request.Quote.Response))
throw new RpcException(new Status(StatusCode.InvalidArgument, "Trigger and response are required"));
if (string.IsNullOrWhiteSpace(request.Quote.Id))
{
var q = await _qs.AddQuoteAsync(request.GuildId,
userId,
(await _client.GetUserAsync(userId))?.Username ?? userId.ToString(),
request.Quote.Trigger,
request.Quote.Response);
return new()
{
Id = new kwum(q.Id).ToString()
};
}
if (!kwum.TryParse(request.Quote.Id, out var qid))
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid quote id"));
await _qs.EditQuoteAsync(
request.GuildId,
new kwum(request.Quote.Id),
request.Quote.Trigger,
request.Quote.Response);
return new()
{
Id = new kwum(qid).ToString()
};
}
public override async Task<Empty> DeleteQuote(DeleteQuoteRequest request, ServerCallContext context)
{
await _qs.DeleteQuoteAsync(request.GuildId, GetUserId(context.RequestHeaders), true, new kwum(request.Id));
return new Empty();
}
}

View File

@@ -0,0 +1,117 @@
using Grpc.Core;
using GreetType = NadekoBot.Services.GreetType;
namespace NadekoBot.GrpcApi;
public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, IGrpcSvc, INService
{
private readonly GreetService _gs;
private readonly DiscordSocketClient _client;
public GreetByeSvc(GreetService gs, DiscordSocketClient client)
{
_gs = gs;
_client = client;
}
public ServerServiceDefinition Bind()
=> GrpcGreet.BindService(this);
private static GrpcGreetSettings ToConf(GreetSettings? conf)
{
if (conf is null)
return new GrpcGreetSettings();
return new GrpcGreetSettings()
{
Message = conf.MessageText,
Type = (GrpcGreetType)conf.GreetType,
ChannelId = conf.ChannelId?.ToString() ?? string.Empty,
IsEnabled = conf.IsEnabled,
};
}
public override async Task<GrpcGreetSettings> GetGreetSettings(GetGreetRequest request, ServerCallContext context)
{
var guildId = request.GuildId;
var conf = await _gs.GetGreetSettingsAsync(guildId, (GreetType)request.Type);
return ToConf(conf);
}
public override async Task<UpdateGreetReply> UpdateGreet(UpdateGreetRequest request, ServerCallContext context)
{
var gid = request.GuildId;
var s = request.Settings;
var msg = s.Message;
var type = GetGreetType(s.Type);
await _gs.SetMessage(gid, GetGreetType(s.Type), msg);
await _gs.SetGreet(gid, ulong.Parse(s.ChannelId), type, s.IsEnabled);
var settings = await _gs.GetGreetSettingsAsync(gid, type);
if (settings is null)
return new()
{
Success = false
};
return new()
{
Success = true
};
}
public override Task<TestGreetReply> TestGreet(TestGreetRequest request, ServerCallContext context)
=> TestGreet(request.GuildId, request.ChannelId, request.UserId, request.Type);
private async Task<TestGreetReply> TestGreet(
ulong guildId,
ulong channelId,
ulong userId,
GrpcGreetType gtDto)
{
var g = _client.GetGuild(guildId) as IGuild;
if (g is null)
{
return new()
{
Error = "Guild doesn't exist",
Success = false,
};
}
var gu = await g.GetUserAsync(userId);
var ch = await g.GetTextChannelAsync(channelId);
if (gu is null || ch is null)
return new TestGreetReply()
{
Error = "Guild or channel doesn't exist",
Success = false,
};
var gt = GetGreetType(gtDto);
await _gs.Test(guildId, gt, ch, gu);
return new TestGreetReply()
{
Success = true
};
}
private static GreetType GetGreetType(GrpcGreetType gtDto)
{
return gtDto switch
{
GrpcGreetType.Greet => GreetType.Greet,
GrpcGreetType.GreetDm => GreetType.GreetDm,
GrpcGreetType.Bye => GreetType.Bye,
GrpcGreetType.Boost => GreetType.Boost,
_ => throw new ArgumentOutOfRangeException(nameof(gtDto), gtDto, null)
};
}
}

View File

@@ -0,0 +1,242 @@
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Modules.Xp.Services;
namespace NadekoBot.GrpcApi;
public static class GrpcApiExtensions
{
public static ulong GetUserId(this ServerCallContext context)
=> ulong.Parse(context.RequestHeaders.FirstOrDefault(x => x.Key == "userid")!.Value);
}
public sealed class OtherSvc : GrpcOther.GrpcOtherBase, IGrpcSvc, INService
{
private readonly IDiscordClient _client;
private readonly XpService _xp;
private readonly ICurrencyService _cur;
private readonly WaifuService _waifus;
private readonly ICoordinator _coord;
private readonly IStatsService _stats;
private readonly IBotCache _cache;
public OtherSvc(
DiscordSocketClient client,
XpService xp,
ICurrencyService cur,
WaifuService waifus,
ICoordinator coord,
IStatsService stats,
IBotCache cache)
{
_client = client;
_xp = xp;
_cur = cur;
_waifus = waifus;
_coord = coord;
_stats = stats;
_cache = cache;
}
public ServerServiceDefinition Bind()
=> GrpcOther.BindService(this);
[GrpcNoAuthRequired]
public override async Task<BotOnGuildReply> BotOnGuild(BotOnGuildRequest request, ServerCallContext context)
{
var guild = await _client.GetGuildAsync(request.GuildId);
var reply = new BotOnGuildReply
{
Success = guild is not null
};
return reply;
}
public override async Task<GetRolesReply> GetRoles(GetRolesRequest request, ServerCallContext context)
{
var g = await _client.GetGuildAsync(request.GuildId);
var roles = g?.Roles;
var reply = new GetRolesReply();
reply.Roles.AddRange(roles?.Select(x => new RoleReply()
{
Id = x.Id,
Name = x.Name,
Color = x.Color.ToString(),
IconUrl = x.GetIconUrl() ?? string.Empty,
}) ?? new List<RoleReply>());
return reply;
}
public override async Task<GetTextChannelsReply> GetTextChannels(
GetTextChannelsRequest request,
ServerCallContext context)
{
var g = await _client.GetGuildAsync(request.GuildId);
var reply = new GetTextChannelsReply();
var chs = await g.GetTextChannelsAsync();
reply.TextChannels.AddRange(chs.Select(x => new TextChannelReply()
{
Id = x.Id,
Name = x.Name,
}));
return reply;
}
[GrpcNoAuthRequired]
public override async Task<GetGuildsReply> GetGuilds(Empty request, ServerCallContext context)
{
var guilds = await _client.GetGuildsAsync(CacheMode.CacheOnly);
var reply = new GetGuildsReply();
var userId = context.GetUserId();
var toReturn = new List<IGuild>();
foreach (var g in guilds)
{
var user = await g.GetUserAsync(userId);
if (user is not null && user.GuildPermissions.Has(GuildPermission.Administrator))
toReturn.Add(g);
}
reply.Guilds.AddRange(toReturn
.Select(x => new GuildReply()
{
Id = x.Id,
Name = x.Name,
IconUrl = x.IconUrl
}));
return reply;
}
[GrpcNoAuthRequired]
public override async Task<CurrencyLbReply> GetCurrencyLb(GetLbRequest request, ServerCallContext context)
{
var users = await _cur.GetTopRichest(_client.CurrentUser.Id, request.Page, request.PerPage);
var reply = new CurrencyLbReply();
var entries = users.Select(async x =>
{
var user = await _client.GetUserAsync(x.UserId, CacheMode.CacheOnly);
return new CurrencyLbEntryReply()
{
Amount = x.CurrencyAmount,
User = user?.ToString() ?? x.Username,
UserId = x.UserId,
Avatar = user?.RealAvatarUrl().ToString() ?? x.RealAvatarUrl()?.ToString()
};
});
reply.Entries.AddRange(await entries.WhenAll());
return reply;
}
[GrpcNoAuthRequired]
public override async Task<XpLbReply> GetXpLb(GetLbRequest request, ServerCallContext context)
{
var users = await _xp.GetGlobalUserXps(request.Page);
var reply = new XpLbReply();
var entries = users.Select(x =>
{
var lvl = new LevelStats(x.TotalXp);
return new XpLbEntryReply()
{
Level = lvl.Level,
TotalXp = x.TotalXp,
User = x.Username,
UserId = x.UserId
};
});
reply.Entries.AddRange(entries);
return reply;
}
[GrpcNoAuthRequired]
public override async Task<WaifuLbReply> GetWaifuLb(GetLbRequest request, ServerCallContext context)
{
var waifus = await _waifus.GetTopWaifusAtPage(request.Page, request.PerPage);
var reply = new WaifuLbReply();
reply.Entries.AddRange(waifus.Select(x => new WaifuLbEntry()
{
ClaimedBy = x.ClaimerName ?? string.Empty,
IsMutual = x.ClaimerName == x.Affinity,
Value = x.Price,
User = x.WaifuName,
}));
return reply;
}
[GrpcNoAuthRequired]
public override async Task<GetShardStatusesReply> GetShardStatuses(Empty request, ServerCallContext context)
{
var reply = new GetShardStatusesReply();
await _cache.GetOrAddAsync<List<ShardStatus>>("coord:statuses",
() => Task.FromResult(_coord.GetAllShardStatuses().ToList())!,
TimeSpan.FromMinutes(1));
var shards = _coord.GetAllShardStatuses();
reply.Shards.AddRange(shards.Select(x => new ShardStatusReply()
{
Id = x.ShardId,
Status = x.ConnectionState.ToString(),
GuildCount = x.GuildCount,
LastUpdate = Timestamp.FromDateTime(x.LastUpdate),
}));
return reply;
}
public override async Task<GetServerInfoReply> GetServerInfo(ServerInfoRequest request, ServerCallContext context)
{
var info = await _stats.GetGuildInfoAsync(request.GuildId);
var reply = new GetServerInfoReply()
{
Id = info.Id,
Name = info.Name,
IconUrl = info.IconUrl,
OwnerId = info.OwnerId,
OwnerName = info.Owner,
TextChannels = info.TextChannels,
VoiceChannels = info.VoiceChannels,
MemberCount = info.MemberCount,
CreatedAt = info.CreatedAt.Ticks,
};
reply.Features.AddRange(info.Features);
reply.Emojis.AddRange(info.Emojis.Select(x => new EmojiReply()
{
Name = x.Name,
Url = x.Url,
Code = x.ToString()
}));
reply.Roles.AddRange(info.Roles.Select(x => new RoleReply()
{
Id = x.Id,
Name = x.Name,
IconUrl = x.GetIconUrl() ?? string.Empty,
Color = x.Color.ToString()
}));
return reply;
}
}

View File

@@ -0,0 +1,224 @@
using Grpc.Core;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Administration.Services;
using Enum = System.Enum;
namespace NadekoBot.GrpcApi;
public sealed class WarnSvc : GrpcWarn.GrpcWarnBase, IGrpcSvc, INService
{
private readonly UserPunishService _ups;
private readonly DiscordSocketClient _client;
public WarnSvc(UserPunishService ups, DiscordSocketClient client)
{
_ups = ups;
_client = client;
}
public ServerServiceDefinition Bind()
=> GrpcWarn.BindService(this);
public override async Task<WarnSettingsReply> GetWarnSettings(
WarnSettingsRequest request,
ServerCallContext context)
{
var list = await _ups.WarnPunishList(request.GuildId);
var wsr = new WarnSettingsReply();
(wsr.ExpiryDays, wsr.DeleteOnExpire) = await _ups.GetWarnExpire(request.GuildId);
wsr.Punishments.AddRange(list.Select(x => new WarnPunishment()
{
Action = x.Punishment.ToString(),
Duration = x.Time,
Threshold = x.Count,
Role = x.RoleId is ulong rid
? _client.GetGuild(request.GuildId)?.GetRole(rid)?.Name ?? x.RoleId?.ToString() ?? string.Empty
: string.Empty
}));
return wsr;
}
public override async Task<SetWarnExpiryReply> SetWarnExpiry(
SetWarnExpiryRequest request,
ServerCallContext context)
{
if (request.ExpiryDays > 366)
{
return new SetWarnExpiryReply()
{
Success = false
};
}
await _ups.WarnExpireAsync(request.GuildId, request.ExpiryDays, request.DeleteOnExpire);
return new SetWarnExpiryReply()
{
Success = true
};
}
public override async Task<DeleteWarnpReply> DeleteWarnp(DeleteWarnpRequest request, ServerCallContext context)
{
var succ = await _ups.WarnPunishRemove(request.GuildId, request.Threshold);
return new DeleteWarnpReply
{
Success = succ
};
}
public override async Task<AddWarnpReply> AddWarnp(AddWarnpRequest request, ServerCallContext context)
{
if (request.Punishment.Threshold <= 0)
throw new RpcException(new Status(StatusCode.InvalidArgument, "Threshold must be greater than 0"));
var g = _client.GetGuild(request.GuildId);
if (g is null)
throw new RpcException(new Status(StatusCode.NotFound, "Guild not found"));
if (!Enum.TryParse<PunishmentAction>(request.Punishment.Action, out var action))
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid action"));
IRole? role = null;
if (action == PunishmentAction.AddRole && ulong.TryParse(request.Punishment.Role, out var roleId))
{
role = g.GetRole(roleId);
if (role is null)
return new AddWarnpReply()
{
Success = false
};
if(!ulong.TryParse(context.RequestHeaders.GetValue("userid"), out var userId))
return new AddWarnpReply()
{
Success = false
};
var user = await ((IGuild)g).GetUserAsync(userId);
if (user is null)
throw new RpcException(new Status(StatusCode.NotFound, "User not found"));
var userMaxRole = user.GetRoles().MaxBy(x => x.Position)?.Position ?? 0;
if (g.OwnerId != user.Id && userMaxRole <= role.Position)
{
return new AddWarnpReply()
{
Success = false
};
}
}
var duration = TimeSpan.FromMinutes(request.Punishment.Duration);
var succ = await _ups.WarnPunish(request.GuildId,
request.Punishment.Threshold,
action,
duration,
role
);
return new AddWarnpReply()
{
Success = succ
};
}
public override async Task<GetLatestWarningsReply> GetLatestWarnings(
GetLatestWarningsRequest request,
ServerCallContext context)
{
var (latest, count) = await _ups.GetLatestWarnings(request.GuildId, request.Page);
var reply = new GetLatestWarningsReply()
{
TotalCount = count
};
reply.Warnings.AddRange(latest.Select(MapWarningToGrpcWarning));
return reply;
}
public override async Task<GetUserWarningsReply> GetUserWarnings(
GetUserWarningsRequest request,
ServerCallContext context)
{
IReadOnlyCollection<Db.Models.Warning> latest = [];
var count = 0;
if (ulong.TryParse(request.User, out var userId))
{
(latest, count) = await _ups.GetUserWarnings(request.GuildId, userId, request.Page);
}
else if (_client.GetGuild(request.GuildId)?.Users.FirstOrDefault(x => x.Username == request.User) is { } user)
{
(latest, count) = await _ups.GetUserWarnings(request.GuildId, user.Id, request.Page);
}
else
{
}
var reply = new GetUserWarningsReply
{
TotalCount = count
};
reply.Warnings.AddRange(latest.Select(MapWarningToGrpcWarning));
return reply;
}
private Warning MapWarningToGrpcWarning(Db.Models.Warning x)
{
return new Warning
{
Id = new kwum(x.Id).ToString(),
Forgiven = x.Forgiven,
ForgivenBy = x.ForgivenBy ?? string.Empty,
Reason = x.Reason ?? string.Empty,
Timestamp = x.DateAdded is { } da ? Nadeko.Common.Extensions.ToTimestamp(da) : 0,
Weight = x.Weight,
Moderator = x.Moderator ?? string.Empty,
User = _client.GetUser(x.UserId)?.Username ?? x.UserId.ToString(),
UserId = x.UserId
};
}
public override async Task<ForgiveWarningReply> ForgiveWarning(
ForgiveWarningRequest request,
ServerCallContext context)
{
if (!kwum.TryParse(request.WarnId, out var wid))
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid warning ID"));
var succ = await _ups.ForgiveWarning(request.GuildId, wid, request.ModName);
return new ForgiveWarningReply
{
Success = succ
};
}
public override async Task<ForgiveWarningReply> DeleteWarning(
ForgiveWarningRequest request,
ServerCallContext context)
{
if (!kwum.TryParse(request.WarnId, out var wid))
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid warning ID"));
var succ = await _ups.WarnDelete(request.GuildId, wid);
return new ForgiveWarningReply
{
Success = succ
};
}
}

View File

@@ -0,0 +1,86 @@
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace NadekoBot.GrpcApi;
public sealed partial class GrpcApiPermsInterceptor : Interceptor
{
private const GuildPerm DEFAULT_PERMISSION = GuildPermission.Administrator;
private readonly DiscordSocketClient _client;
public GrpcApiPermsInterceptor(DiscordSocketClient client)
{
_client = client;
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
try
{
var method = context.Method[(context.Method.LastIndexOf('/') + 1)..];
// get metadata
var metadata = context
.RequestHeaders
.ToDictionary(x => x.Key, x => x.Value);
Log.Information("grpc | g: {GuildId} | u: {UserID} | cmd: {Method}",
metadata.TryGetValue("guildid", out var gidString) ? gidString : "none",
metadata.TryGetValue("userid", out var uidString) ? uidString : "none",
method);
// there always has to be a user who makes the call
if (!metadata.ContainsKey("userid"))
throw new RpcException(new(StatusCode.Unauthenticated, "userid has to be specified."));
// get the method name without the service name
// if the method is explicitly marked as not requiring auth
if (_noAuthRequired.Contains(method))
return await continuation(request, context);
// otherwise the method requires auth, and if it requires auth then the guildid has to be specified
if (string.IsNullOrWhiteSpace(gidString))
throw new RpcException(new(StatusCode.Unauthenticated, "guildid has to be specified."));
var userId = ulong.Parse(metadata["userid"]);
var guildId = ulong.Parse(gidString);
// check if the user has the required permission
if (_perms.TryGetValue(method, out var perm))
{
await EnsureUserHasPermission(guildId, userId, perm);
}
else
{
// if not then use the default, which is Administrator permission
await EnsureUserHasPermission(guildId, userId, DEFAULT_PERMISSION);
}
return await continuation(request, context);
}
catch (Exception ex)
{
Log.Error(ex, "Error thrown by {ContextMethod}", context.Method);
throw;
}
}
private async Task EnsureUserHasPermission(ulong guildId, ulong userId, GuildPerm perm)
{
IGuild guild = _client.GetGuild(guildId);
var user = guild is null ? null : await guild.GetUserAsync(userId);
if (user is null)
throw new RpcException(new Status(StatusCode.NotFound, "User not found"));
if (!user.GuildPermissions.Has(perm))
throw new RpcException(new Status(StatusCode.PermissionDenied,
$"You need {perm} permission to use this method"));
}
}

View File

@@ -0,0 +1,75 @@
using Grpc.Core;
using Grpc.Core.Interceptors;
using NadekoBot.Common.ModuleBehaviors;
namespace NadekoBot.GrpcApi;
public class GrpcApiService : INService, IReadyExecutor
{
private Server? _app;
private readonly DiscordSocketClient _client;
private readonly IEnumerable<IGrpcSvc> _svcs;
private readonly IBotCredsProvider _creds;
public GrpcApiService(
DiscordSocketClient client,
IEnumerable<IGrpcSvc> svcs,
IBotCredsProvider creds)
{
_client = client;
_svcs = svcs;
_creds = creds;
}
public Task OnReadyAsync()
{
var creds = _creds.GetCreds();
if (creds.GrpcApi is null || !creds.GrpcApi.Enabled)
return Task.CompletedTask;
try
{
var host = creds.GrpcApi.Host;
var port = creds.GrpcApi.Port + _client.ShardId;
var interceptor = new GrpcApiPermsInterceptor(_client);
var serverCreds = ServerCredentials.Insecure;
if (creds.GrpcApi is
{
CertPrivateKey: not null and not "",
CertChain: not null and not ""
} cert)
{
serverCreds = new SslServerCredentials(
new[] { new KeyCertificatePair(cert.CertChain, cert.CertPrivateKey) });
}
_app = new()
{
Ports =
{
new(host, port, serverCreds),
}
};
foreach (var svc in _svcs)
{
_app.Services.Add(svc.Bind().Intercept(interceptor));
}
_app.Start();
Log.Information("Grpc Api Server started on port {Host}:{Port}", host, port);
}
catch (Exception ex)
{
Log.Error(ex, "Error starting Grpc Api Server");
_app?.ShutdownAsync().GetAwaiter().GetResult();
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,8 @@
using Grpc.Core;
namespace NadekoBot.GrpcApi;
public interface IGrpcSvc
{
ServerServiceDefinition Bind();
}

View File

@@ -6,9 +6,9 @@ namespace Nadeko.Common;
public static class LogSetup public static class LogSetup
{ {
public static void SetupLogger(object source) public static void SetupLogger(object source, IBotCreds creds)
{ {
Log.Logger = new LoggerConfiguration().MinimumLevel.Override("Microsoft", LogEventLevel.Information) var config = new LoggerConfiguration().MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("System", LogEventLevel.Information) .MinimumLevel.Override("System", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.Enrich.FromLogContext() .Enrich.FromLogContext()
@@ -16,8 +16,13 @@ public static class LogSetup
theme: GetTheme(), theme: GetTheme(),
outputTemplate: outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] | #{LogSource} | {Message:lj}{NewLine}{Exception}") "[{Timestamp:HH:mm:ss} {Level:u3}] | #{LogSource} | {Message:lj}{NewLine}{Exception}")
.Enrich.WithProperty("LogSource", source) .Enrich.WithProperty("LogSource", source);
.CreateLogger();
if (!string.IsNullOrWhiteSpace(creds.Seq.Url))
config = config.WriteTo.Seq(creds.Seq.Url, apiKey: creds.Seq.ApiKey);
Log.Logger = config
.CreateLogger();
Console.OutputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8;
} }

View File

@@ -1,7 +1,7 @@
#nullable disable #nullable disable
namespace NadekoBot; namespace NadekoBot;
public interface IBotCredentials public interface IBotCreds
{ {
string Token { get; } string Token { get; }
string NadekoAiToken { get; } string NadekoAiToken { get; }
@@ -29,6 +29,8 @@ public interface IBotCredentials
string TwitchClientSecret { get; set; } string TwitchClientSecret { get; set; }
GoogleApiConfig Google { get; set; } GoogleApiConfig Google { get; set; }
BotCacheImplemenation BotCache { get; set; } BotCacheImplemenation BotCache { get; set; }
Creds.GrpcApiConfig GrpcApi { get; set; }
SeqConfig Seq { get; set; }
} }
public interface IVotesSettings public interface IVotesSettings

View File

@@ -3,6 +3,6 @@
public interface IBotCredsProvider public interface IBotCredsProvider
{ {
public void Reload(); public void Reload();
public IBotCredentials GetCreds(); public IBotCreds GetCreds();
public void ModifyCredsFile(Action<IBotCredentials> func); public void ModifyCredsFile(Action<IBotCreds> func);
} }

View File

@@ -29,7 +29,7 @@ public sealed partial class BotConfig : ICloneable<BotConfig>
public CultureInfo DefaultLocale { get; set; } public CultureInfo DefaultLocale { get; set; }
[Comment(""" [Comment("""
Style in which executed commands will show up in the console. Style in which executed commands will show up in the logs.
Allowed values: Simple, Normal, None Allowed values: Simple, Normal, None
""")] """)]
public ConsoleOutputType ConsoleOutputType { get; set; } public ConsoleOutputType ConsoleOutputType { get; set; }

View File

@@ -3,30 +3,31 @@ using NadekoBot.Common.Yml;
namespace NadekoBot.Common; namespace NadekoBot.Common;
public sealed class Creds : IBotCredentials public sealed class Creds : IBotCreds
{ {
[Comment("""DO NOT CHANGE""")] [Comment("""DO NOT CHANGE""")]
public int Version { get; set; } public int Version { get; set; } = 13;
[Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")] [Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")]
public string Token { get; set; } public string Token { get; set; }
[Comment(""" [Comment("""
List of Ids of the users who have bot owner permissions List of Ids of the users who have bot owner permissions
**DO NOT ADD PEOPLE YOU DON'T TRUST** **DO NOT ADD PEOPLE YOU DON'T TRUST**
""")] """)]
public ICollection<ulong> OwnerIds { get; set; } public ICollection<ulong> OwnerIds { get; set; }
[Comment("Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted")] [Comment(
"Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted")]
public bool UsePrivilegedIntents { get; set; } public bool UsePrivilegedIntents { get; set; }
[Comment(""" [Comment("""
The number of shards that the bot will be running on. The number of shards that the bot will be running on.
Leave at 1 if you don't know what you're doing. Leave at 1 if you don't know what you're doing.
note: If you are planning to have more than one shard, then you must change botCache to 'redis'. note: If you are planning to have more than one shard, then you must change botCache to 'redis'.
Also, in that case you should be using NadekoBot.Coordinator to start the bot, and it will correctly override this value. Also, in that case you should be using NadekoBot.Coordinator to start the bot, and it will correctly override this value.
""")] """)]
public int TotalShards { get; set; } public int TotalShards { get; set; }
[Comment(""" [Comment("""
@@ -37,34 +38,34 @@ public sealed class Creds : IBotCredentials
For example '@Bot how's the weather in Paris' will return the current weather in Paris as if you were to run `.weather Paris` command. For example '@Bot how's the weather in Paris' will return the current weather in Paris as if you were to run `.weather Paris` command.
""")] """)]
public string NadekoAiToken { get; set; } public string NadekoAiToken { get; set; }
[Comment( [Comment(
""" """
Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it. Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
Then, go to APIs and Services -> Credentials and click Create credentials -> API key. Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
Used only for Youtube Data Api (at the moment). Used only for Youtube Data Api (at the moment).
""")] """)]
public string GoogleApiKey { get; set; } public string GoogleApiKey { get; set; }
[Comment( [Comment(
""" """
Create a new custom search here https://programmablesearchengine.google.com/cse/create/new Create a new custom search here https://programmablesearchengine.google.com/cse/create/new
Enable SafeSearch Enable SafeSearch
Remove all Sites to Search Remove all Sites to Search
Enable Search the entire web Enable Search the entire web
Copy the 'Search Engine ID' to the SearchId field Copy the 'Search Engine ID' to the SearchId field
Do all steps again but enable image search for the ImageSearchId Do all steps again but enable image search for the ImageSearchId
""")] """)]
public GoogleApiConfig Google { get; set; } public GoogleApiConfig Google { get; set; }
[Comment("""Settings for voting system for discordbots. Meant for use on global Nadeko.""")] [Comment("""Settings for voting system for discordbots. Meant for use on global Nadeko.""")]
public VotesSettings Votes { get; set; } public VotesSettings Votes { get; set; }
[Comment(""" [Comment("""
Patreon auto reward system settings. Patreon auto reward system settings.
go to https://www.patreon.com/portal -> my clients -> create client go to https://www.patreon.com/portal -> my clients -> create client
""")] """)]
public PatreonSettings Patreon { get; set; } public PatreonSettings Patreon { get; set; }
[Comment("""Api key for sending stats to DiscordBotList.""")] [Comment("""Api key for sending stats to DiscordBotList.""")]
@@ -75,27 +76,27 @@ public sealed class Creds : IBotCredentials
[Comment(@"OpenAi api key.")] [Comment(@"OpenAi api key.")]
public string Gpt3ApiKey { get; set; } public string Gpt3ApiKey { get; set; }
[Comment(""" [Comment("""
Which cache implementation should bot use. Which cache implementation should bot use.
'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset. 'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset.
'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml 'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml
""")] """)]
public BotCacheImplemenation BotCache { get; set; } public BotCacheImplemenation BotCache { get; set; }
[Comment(""" [Comment("""
Redis connection string. Don't change if you don't know what you're doing. Redis connection string. Don't change if you don't know what you're doing.
Only used if botCache is set to 'redis' Only used if botCache is set to 'redis'
""")] """)]
public string RedisOptions { get; set; } public string RedisOptions { get; set; }
[Comment("""Database options. Don't change if you don't know what you're doing. Leave null for default values""")] [Comment("""Database options. Don't change if you don't know what you're doing. Leave null for default values""")]
public DbOptions Db { get; set; } public DbOptions Db { get; set; }
[Comment(""" [Comment("""
Address and port of the coordinator endpoint. Leave empty for default. Address and port of the coordinator endpoint. Leave empty for default.
Change only if you've changed the coordinator address or port. Change only if you've changed the coordinator address or port.
""")] """)]
public string CoordinatorUrl { get; set; } public string CoordinatorUrl { get; set; }
[Comment( [Comment(
@@ -103,23 +104,23 @@ public sealed class Creds : IBotCredentials
public string RapidApiKey { get; set; } public string RapidApiKey { get; set; }
[Comment(""" [Comment("""
https://locationiq.com api key (register and you will receive the token in the email). https://locationiq.com api key (register and you will receive the token in the email).
Used only for .time command. Used only for .time command.
""")] """)]
public string LocationIqApiKey { get; set; } public string LocationIqApiKey { get; set; }
[Comment(""" [Comment("""
https://timezonedb.com api key (register and you will receive the token in the email). https://timezonedb.com api key (register and you will receive the token in the email).
Used only for .time command Used only for .time command
""")] """)]
public string TimezoneDbApiKey { get; set; } public string TimezoneDbApiKey { get; set; }
[Comment(""" [Comment("""
https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use. https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use.
Used for cryptocurrency related commands. Used for cryptocurrency related commands.
""")] """)]
public string CoinmarketcapApiKey { get; set; } public string CoinmarketcapApiKey { get; set; }
// [Comment(@"https://polygon.io/dashboard/api-keys api key. Free plan allows for 5 queries per minute. // [Comment(@"https://polygon.io/dashboard/api-keys api key. Free plan allows for 5 queries per minute.
// Used for stocks related commands.")] // Used for stocks related commands.")]
// public string PolygonIoApiKey { get; set; } // public string PolygonIoApiKey { get; set; }
@@ -128,9 +129,9 @@ public sealed class Creds : IBotCredentials
public string OsuApiKey { get; set; } public string OsuApiKey { get; set; }
[Comment(""" [Comment("""
Optional Trovo client id. Optional Trovo client id.
You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors. You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors.
""")] """)]
public string TrovoClientId { get; set; } public string TrovoClientId { get; set; }
[Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")] [Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")]
@@ -140,23 +141,35 @@ public sealed class Creds : IBotCredentials
public string TwitchClientSecret { get; set; } public string TwitchClientSecret { get; set; }
[Comment(""" [Comment("""
Command and args which will be used to restart the bot. Command and args which will be used to restart the bot.
Only used if bot is executed directly (NOT through the coordinator) Only used if bot is executed directly (NOT through the coordinator)
placeholders: placeholders:
{0} -> shard id {0} -> shard id
{1} -> total shards {1} -> total shards
Linux default Linux default
cmd: dotnet cmd: dotnet
args: "NadekoBot.dll -- {0}" args: "NadekoBot.dll -- {0}"
Windows default Windows default
cmd: NadekoBot.exe cmd: NadekoBot.exe
args: "{0}" args: "{0}"
""")] """)]
public RestartConfig RestartCommand { get; set; } public RestartConfig RestartCommand { get; set; }
[Comment("""
Settings for the grpc api.
We don't provide support for this.
If you leave certPath empty, the api will run on http.
""")]
public GrpcApiConfig GrpcApi { get; set; }
[Comment("""
Url and api key to a seq server. If url is set, bot will try to send logs to it.
""")]
public SeqConfig Seq { get; set; }
public Creds() public Creds()
{ {
Version = 9;
Token = string.Empty; Token = string.Empty;
UsePrivilegedIntents = true; UsePrivilegedIntents = true;
OwnerIds = new List<ulong>(); OwnerIds = new List<ulong>();
@@ -179,24 +192,27 @@ public sealed class Creds : IBotCredentials
RestartCommand = new RestartConfig(); RestartCommand = new RestartConfig();
Google = new GoogleApiConfig(); Google = new GoogleApiConfig();
GrpcApi = new();
Seq = new();
} }
public class DbOptions public class DbOptions
: IDbOptions : IDbOptions
{ {
[Comment(""" [Comment("""
Database type. "sqlite", "mysql" and "postgresql" are supported. Database type. "sqlite", "mysql" and "postgresql" are supported.
Default is "sqlite" Default is "sqlite"
""")] """)]
public string Type { get; set; } public string Type { get; set; }
[Comment(""" [Comment("""
Database connection string. Database connection string.
You MUST change this if you're not using "sqlite" type. You MUST change this if you're not using "sqlite" type.
Default is "Data Source=data/NadekoBot.db" Default is "Data Source=data/NadekoBot.db"
Example for mysql: "Server=localhost;Port=3306;Uid=root;Pwd=my_super_secret_mysql_password;Database=nadeko" Example for mysql: "Server=localhost;Port=3306;Uid=root;Pwd=my_super_secret_mysql_password;Database=nadeko"
Example for postgresql: "Server=localhost;Port=5432;User Id=postgres;Password=my_super_secret_postgres_password;Database=nadeko;" Example for postgresql: "Server=localhost;Port=5432;User Id=postgres;Password=my_super_secret_postgres_password;Database=nadeko;"
""")] """)]
public string ConnectionString { get; set; } public string ConnectionString { get; set; }
} }
@@ -231,29 +247,29 @@ public sealed class Creds : IBotCredentials
public sealed record VotesSettings : IVotesSettings public sealed record VotesSettings : IVotesSettings
{ {
[Comment(""" [Comment("""
top.gg votes service url top.gg votes service url
This is the url of your instance of the NadekoBot.Votes api This is the url of your instance of the NadekoBot.Votes api
Example: https://votes.my.cool.bot.com Example: https://votes.my.cool.bot.com
""")] """)]
public string TopggServiceUrl { get; set; } public string TopggServiceUrl { get; set; }
[Comment(""" [Comment("""
Authorization header value sent to the TopGG service url with each request Authorization header value sent to the TopGG service url with each request
This should be equivalent to the TopggKey in your NadekoBot.Votes api appsettings.json file This should be equivalent to the TopggKey in your NadekoBot.Votes api appsettings.json file
""")] """)]
public string TopggKey { get; set; } public string TopggKey { get; set; }
[Comment(""" [Comment("""
discords.com votes service url discords.com votes service url
This is the url of your instance of the NadekoBot.Votes api This is the url of your instance of the NadekoBot.Votes api
Example: https://votes.my.cool.bot.com Example: https://votes.my.cool.bot.com
""")] """)]
public string DiscordsServiceUrl { get; set; } public string DiscordsServiceUrl { get; set; }
[Comment(""" [Comment("""
Authorization header value sent to the Discords service url with each request Authorization header value sent to the Discords service url with each request
This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsettings.json file This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsettings.json file
""")] """)]
public string DiscordsKey { get; set; } public string DiscordsKey { get; set; }
public VotesSettings() public VotesSettings()
@@ -272,13 +288,25 @@ public sealed class Creds : IBotCredentials
DiscordsKey = discordsKey; DiscordsKey = discordsKey;
} }
} }
public sealed record GrpcApiConfig
{
public bool Enabled { get; set; } = false;
public string CertChain { get; set; } = string.Empty;
public string CertPrivateKey { get; set; } = string.Empty;
public string Host { get; set; } = "localhost";
public int Port { get; set; } = 43120;
}
}
public sealed class SeqConfig
{
public string Url { get; init; }
public string ApiKey { get; init; }
} }
public class GoogleApiConfig : IGoogleApiConfig public class GoogleApiConfig : IGoogleApiConfig
{ {
public string SearchId { get; init; } public string SearchId { get; init; }
public string ImageSearchId { get; init; } public string ImageSearchId { get; init; }
} }

View File

@@ -14,6 +14,7 @@ public class DownloadTracker : INService
public async Task EnsureUsersDownloadedAsync(IGuild guild) public async Task EnsureUsersDownloadedAsync(IGuild guild)
{ {
#if GLOBAL_NADEKO #if GLOBAL_NADEKO
await Task.CompletedTask;
return; return;
#endif #endif
await _downloadUsersSemaphore.WaitAsync(); await _downloadUsersSemaphore.WaitAsync();

View File

@@ -119,7 +119,7 @@ public sealed class BotCredsProvider : IBotCredsProvider
} }
} }
public void ModifyCredsFile(Action<IBotCredentials> func) public void ModifyCredsFile(Action<IBotCreds> func)
{ {
var ymlData = File.ReadAllText(CREDS_FILE_NAME); var ymlData = File.ReadAllText(CREDS_FILE_NAME);
var creds = Yaml.Deserializer.Deserialize<Creds>(ymlData); var creds = Yaml.Deserializer.Deserialize<Creds>(ymlData);
@@ -137,24 +137,18 @@ public sealed class BotCredsProvider : IBotCredsProvider
var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(CREDS_FILE_NAME)); var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(CREDS_FILE_NAME));
if (creds.Version <= 5) if (creds.Version <= 5)
{ {
creds.BotCache = BotCacheImplemenation.Redis; creds.BotCache = BotCacheImplemenation.Memory;
} }
if (creds.Version <= 6) if (creds.Version < 13)
{ {
creds.Version = 7; creds.Version = 13;
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
}
if (creds.Version <= 8)
{
creds.Version = 9;
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds)); File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
} }
} }
} }
public IBotCredentials GetCreds() public IBotCreds GetCreds()
{ {
lock (_reloadLock) lock (_reloadLock)
{ {

View File

@@ -75,7 +75,7 @@ public sealed partial class GoogleApiService : IGoogleApiService, INService
return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId).Skip(1); return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId).Skip(1);
} }
public async Task<IEnumerable<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1) public async Task<IReadOnlyList<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1)
{ {
if (string.IsNullOrWhiteSpace(keywords)) if (string.IsNullOrWhiteSpace(keywords))
throw new ArgumentNullException(nameof(keywords)); throw new ArgumentNullException(nameof(keywords));
@@ -87,7 +87,7 @@ public sealed partial class GoogleApiService : IGoogleApiService, INService
query.Q = keywords; query.Q = keywords;
query.Type = "video"; query.Type = "video";
query.SafeSearch = SearchResource.ListRequest.SafeSearchEnum.Strict; query.SafeSearch = SearchResource.ListRequest.SafeSearchEnum.Strict;
return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId); return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId).ToArray();
} }
public async Task<IEnumerable<(string Name, string Id, string Url, string Thumbnail)>> GetVideoInfosByKeywordAsync( public async Task<IEnumerable<(string Name, string Id, string Url, string Thumbnail)>> GetVideoInfosByKeywordAsync(

View File

@@ -4,11 +4,11 @@ namespace NadekoBot.Common;
public sealed class RedisPubSub : IPubSub public sealed class RedisPubSub : IPubSub
{ {
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly ConnectionMultiplexer _multi; private readonly ConnectionMultiplexer _multi;
private readonly ISeria _serializer; private readonly ISeria _serializer;
public RedisPubSub(ConnectionMultiplexer multi, ISeria serializer, IBotCredentials creds) public RedisPubSub(ConnectionMultiplexer multi, ISeria serializer, IBotCreds creds)
{ {
_multi = multi; _multi = multi;
_serializer = serializer; _serializer = serializer;

View File

@@ -15,13 +15,13 @@ public class RedisBotStringsProvider : IBotStringsProvider
private readonly ConnectionMultiplexer _redis; private readonly ConnectionMultiplexer _redis;
private readonly IStringsSource _source; private readonly IStringsSource _source;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
public RedisBotStringsProvider( public RedisBotStringsProvider(
ConnectionMultiplexer redis, ConnectionMultiplexer redis,
DiscordSocketClient discordClient, DiscordSocketClient discordClient,
IStringsSource source, IStringsSource source,
IBotCredentials creds) IBotCreds creds)
{ {
_redis = redis; _redis = redis;
_source = source; _source = source;

View File

@@ -11,7 +11,7 @@ public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor
private readonly Coordinator.Coordinator.CoordinatorClient _coordClient; private readonly Coordinator.Coordinator.CoordinatorClient _coordClient;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
public RemoteGrpcCoordinator(IBotCredentials creds, DiscordSocketClient client) public RemoteGrpcCoordinator(IBotCreds creds, DiscordSocketClient client)
{ {
var coordUrl = string.IsNullOrWhiteSpace(creds.CoordinatorUrl) ? "http://localhost:3442" : creds.CoordinatorUrl; var coordUrl = string.IsNullOrWhiteSpace(creds.CoordinatorUrl) ? "http://localhost:3442" : creds.CoordinatorUrl;
@@ -90,8 +90,7 @@ public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor
{ {
if (!gracefulImminent) if (!gracefulImminent)
{ {
Log.Warning(ex, Log.Warning(ex, "Hearbeat failed and graceful shutdown was not expected: {Message}",
"Hearbeat failed and graceful shutdown was not expected: {Message}",
ex.Message); ex.Message);
break; break;
} }

View File

@@ -65,14 +65,16 @@ public sealed partial class ReplacementPatternStore
Register("%user.mention%", static (IUser user) => user.Mention); Register("%user.mention%", static (IUser user) => user.Mention);
Register("%user.fullname%", static (IUser user) => user.ToString()!); Register("%user.fullname%", static (IUser user) => user.ToString()!);
Register("%user.name%", static (IUser user) => user.Username); Register("%user.name%", static (IUser user) => user.Username);
Register("%user.displayname%", static (IUser user) => user is IGuildUser gu ? gu.DisplayName : user.Username);
Register("%user.discrim%", static (IUser user) => user.Discriminator); Register("%user.discrim%", static (IUser user) => user.Discriminator);
Register("%user.avatar%", static (IUser user) => user.RealAvatarUrl().ToString()); Register("%user.avatar%", static (IUser user) => user.RealAvatarUrl().ToString());
Register("%user.id%", static (IUser user) => user.Id.ToString()); Register("%user.id%", static (IUser user) => user.Id.ToString());
Register("%user.created_time%", static (IUser user) => user.CreatedAt.ToString("HH:mm")); Register("%user.created_time%", static (IUser user) => user.CreatedAt.ToString("HH:mm"));
Register("%user.created_date%", static (IUser user) => user.CreatedAt.ToString("dd.MM.yyyy")); Register("%user.created_date%", static (IUser user) => user.CreatedAt.ToString("dd.MM.yyyy"));
Register("%user.joined_time%", static (IGuildUser user) => user.JoinedAt?.ToString("HH:mm")); Register("%user.joined_time%", static (IGuildUser user) => user.JoinedAt?.ToString("HH:mm") ?? "??:??");
Register("%user.joined_date%", static (IGuildUser user) => user.JoinedAt?.ToString("dd.MM.yyyy")); Register("%user.joined_date%",
static (IGuildUser user) => user.JoinedAt?.ToString("dd.MM.yyyy") ?? "??.??.????");
Register("%user%", Register("%user%",
static (IUser[] users) => string.Join(" ", users.Select(user => user.Mention))); static (IUser[] users) => string.Join(" ", users.Select(user => user.Mention)));
Register("%user.mention%", Register("%user.mention%",

View File

@@ -287,9 +287,9 @@ public sealed partial class ResponseBuilder
return this; return this;
} }
public ResponseBuilder UserBasedMentions() public ResponseBuilder UserBasedMentions(IGuildUser? permUser = null)
{ {
sanitizeMentions = !((InternalResolveUser() as IGuildUser)?.GuildPermissions.MentionEveryone ?? false); sanitizeMentions = !((InternalResolveUser() as IGuildUser ?? permUser)?.GuildPermissions.MentionEveryone ?? false);
return this; return this;
} }

View File

@@ -61,7 +61,7 @@ public static class ServiceCollectionExtensions
return svcs; return svcs;
} }
public static IContainer AddCache(this IContainer cont, IBotCredentials creds) public static IContainer AddCache(this IContainer cont, IBotCreds creds)
{ {
if (creds.BotCache == BotCacheImplemenation.Redis) if (creds.BotCache == BotCacheImplemenation.Redis)
{ {

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