Compare commits

..

19 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
51 changed files with 8031 additions and 496 deletions

View File

@@ -2,6 +2,38 @@
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 ## [5.1.13] - 03.10.2024
### Fixed ### Fixed

View File

@@ -12,13 +12,14 @@ namespace NadekoBot.Generators
{ {
public readonly record struct MethodPermData public readonly record struct MethodPermData
{ {
public readonly string Name; public readonly ImmutableArray<(string Name, string Value)> MethodPerms;
public readonly string Value; public readonly ImmutableArray<string> NoAuthRequired;
public MethodPermData(string name, string value) public MethodPermData(ImmutableArray<(string Name, string Value)> methodPerms,
ImmutableArray<string> noAuthRequired)
{ {
Name = name; MethodPerms = methodPerms;
Value = value; NoAuthRequired = noAuthRequired;
} }
} }
@@ -26,7 +27,7 @@ namespace NadekoBot.Generators
[Generator] [Generator]
public class GrpcApiPermGenerator : IIncrementalGenerator public class GrpcApiPermGenerator : IIncrementalGenerator
{ {
public const string Attribute = public const string GRPC_API_PERM_ATTRIBUTE =
""" """
namespace NadekoBot.GrpcApi; namespace NadekoBot.GrpcApi;
@@ -38,12 +39,25 @@ namespace NadekoBot.Generators
} }
"""; """;
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) public void Initialize(IncrementalGeneratorInitializationContext context)
{ {
context.RegisterPostInitializationOutput(ctx => ctx.AddSource("GrpcApiPermAttribute.cs", context.RegisterPostInitializationOutput(ctx => ctx.AddSource("GrpcApiPermAttribute.cs",
SourceText.From(Attribute, Encoding.UTF8))); SourceText.From(GRPC_API_PERM_ATTRIBUTE, Encoding.UTF8)));
var enumsToGenerate = context.SyntaxProvider context.RegisterPostInitializationOutput(ctx => ctx.AddSource("GrpcNoAuthRequiredAttribute.cs",
SourceText.From(GRPC_NO_AUTH_REQUIRED_ATTRIBUTE, Encoding.UTF8)));
var perms = context.SyntaxProvider
.ForAttributeWithMetadataName( .ForAttributeWithMetadataName(
"NadekoBot.GrpcApi.GrpcApiPermAttribute", "NadekoBot.GrpcApi.GrpcApiPermAttribute",
predicate: static (s, _) => s is MethodDeclarationSyntax, predicate: static (s, _) => s is MethodDeclarationSyntax,
@@ -52,11 +66,24 @@ namespace NadekoBot.Generators
.Select(static (x, _) => x!.Value) .Select(static (x, _) => x!.Value)
.Collect(); .Collect();
context.RegisterSourceOutput(enumsToGenerate,
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)); static (spc, source) => Execute(source, spc));
} }
private static MethodPermData? GetMethodSemanticTargets(SemanticModel model, SyntaxNode node) 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 method = (MethodDeclarationSyntax)node;
@@ -64,20 +91,14 @@ namespace NadekoBot.Generators
var attr = method.AttributeLists var attr = method.AttributeLists
.SelectMany(x => x.Attributes) .SelectMany(x => x.Attributes)
.FirstOrDefault(); .FirstOrDefault();
// .FirstOrDefault(x => x.Name.ToString() == "GrpcApiPermAttribute");
if (attr is null) if (attr is null)
return null; return null;
// if (model.GetSymbolInfo(attr).Symbol is not IMethodSymbol attrSymbol) return (name, attr.ArgumentList?.Arguments[0].ToString() ?? "__missing_perm__");
// return null;
return new MethodPermData(name, attr.ArgumentList?.Arguments[0].ToString() ?? "__missing_perm__");
// return new MethodPermData(name, attrSymbol.Parameters[0].ContainingType.ToDisplayString() + "." + attrSymbol.Parameters[0].Name);
} }
private static void Execute(ImmutableArray<MethodPermData> fields, SourceProductionContext ctx) private static void Execute(MethodPermData data, SourceProductionContext ctx)
{ {
using (var stringWriter = new StringWriter()) using (var stringWriter = new StringWriter())
using (var sw = new IndentedTextWriter(stringWriter)) using (var sw = new IndentedTextWriter(stringWriter))
@@ -87,16 +108,17 @@ namespace NadekoBot.Generators
sw.WriteLine("namespace NadekoBot.GrpcApi;"); sw.WriteLine("namespace NadekoBot.GrpcApi;");
sw.WriteLine(); sw.WriteLine();
sw.WriteLine("public partial class PermsInterceptor"); sw.WriteLine("public partial class GrpcApiPermsInterceptor");
sw.WriteLine("{"); sw.WriteLine("{");
sw.Indent++; sw.Indent++;
sw.WriteLine("public static FrozenDictionary<string, GuildPerm> perms = new Dictionary<string, GuildPerm>()"); sw.WriteLine(
"private static FrozenDictionary<string, GuildPerm> _perms = new Dictionary<string, GuildPerm>()");
sw.WriteLine("{"); sw.WriteLine("{");
sw.Indent++; sw.Indent++;
foreach (var field in fields) foreach (var field in data.MethodPerms)
{ {
sw.WriteLine("{{ \"{0}\", {1} }},", field.Name, field.Value); sw.WriteLine("{{ \"{0}\", {1} }},", field.Name, field.Value);
} }
@@ -104,6 +126,21 @@ namespace NadekoBot.Generators
sw.Indent--; sw.Indent--;
sw.WriteLine("}.ToFrozenDictionary();"); 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.Indent--;
sw.WriteLine("}"); sw.WriteLine("}");

View File

@@ -10,6 +10,10 @@ service GrpcExprs {
rpc GetExprs(GetExprsRequest) returns (GetExprsReply); rpc GetExprs(GetExprsRequest) returns (GetExprsReply);
rpc AddExpr(AddExprRequest) returns (AddExprReply); rpc AddExpr(AddExprRequest) returns (AddExprReply);
rpc DeleteExpr(DeleteExprRequest) returns (google.protobuf.Empty); 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 { message DeleteExprRequest {
@@ -48,3 +52,38 @@ message AddExprReply {
string id = 1; string id = 1;
bool success = 2; 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

@@ -5,20 +5,13 @@ option csharp_namespace = "NadekoBot.GrpcApi";
package greet; package greet;
service GrpcGreet { service GrpcGreet {
rpc GetGreetSettings (GetGreetRequest) returns (GetGreetReply); rpc GetGreetSettings (GetGreetRequest) returns (GrpcGreetSettings);
rpc UpdateGreet (UpdateGreetRequest) returns (UpdateGreetReply); rpc UpdateGreet (UpdateGreetRequest) returns (UpdateGreetReply);
rpc TestGreet (TestGreetRequest) returns (TestGreetReply); rpc TestGreet (TestGreetRequest) returns (TestGreetReply);
} }
message GetGreetReply {
GrpcGreetSettings greet = 1;
GrpcGreetSettings greetDm = 2;
GrpcGreetSettings bye = 3;
GrpcGreetSettings boost = 4;
}
message GrpcGreetSettings { message GrpcGreetSettings {
optional uint64 channelId = 1; string channelId = 1;
string message = 2; string message = 2;
bool isEnabled = 3; bool isEnabled = 3;
GrpcGreetType type = 4; GrpcGreetType type = 4;
@@ -26,6 +19,7 @@ message GrpcGreetSettings {
message GetGreetRequest { message GetGreetRequest {
uint64 guildId = 1; uint64 guildId = 1;
GrpcGreetType type = 2;
} }
message UpdateGreetRequest { message UpdateGreetRequest {
@@ -41,7 +35,7 @@ enum GrpcGreetType {
} }
message UpdateGreetReply { message UpdateGreetReply {
bool success = 1; bool Success = 1;
} }
message TestGreetRequest { message TestGreetRequest {

View File

@@ -8,9 +8,10 @@ import "google/protobuf/timestamp.proto";
package other; package other;
service GrpcOther { service GrpcOther {
rpc BotOnGuild(BotOnGuildRequest) returns (BotOnGuildReply);
rpc GetGuilds(google.protobuf.Empty) returns (GetGuildsReply); rpc GetGuilds(google.protobuf.Empty) returns (GetGuildsReply);
rpc GetTextChannels(GetTextChannelsRequest) returns (GetTextChannelsReply); rpc GetTextChannels(GetTextChannelsRequest) returns (GetTextChannelsReply);
rpc GetRoles(GetRolesRequest) returns (GetRolesReply);
rpc GetCurrencyLb(GetLbRequest) returns (CurrencyLbReply); rpc GetCurrencyLb(GetLbRequest) returns (CurrencyLbReply);
rpc GetXpLb(GetLbRequest) returns (XpLbReply); rpc GetXpLb(GetLbRequest) returns (XpLbReply);
@@ -20,6 +21,22 @@ service GrpcOther {
rpc GetServerInfo(ServerInfoRequest) returns (GetServerInfoReply); 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 { message GetGuildsReply {
repeated GuildReply guilds = 1; repeated GuildReply guilds = 1;
} }

View File

@@ -6,11 +6,17 @@ package warn;
service GrpcWarn { service GrpcWarn {
rpc GetWarnSettings (WarnSettingsRequest) returns (WarnSettingsReply); rpc GetWarnSettings (WarnSettingsRequest) returns (WarnSettingsReply);
rpc SetWarnExpiry(SetWarnExpiryRequest) returns (SetWarnExpiryReply);
rpc AddWarnp (AddWarnpRequest) returns (AddWarnpReply); rpc AddWarnp (AddWarnpRequest) returns (AddWarnpReply);
rpc DeleteWarnp (DeleteWarnpRequest) returns (DeleteWarnpReply); rpc DeleteWarnp (DeleteWarnpRequest) returns (DeleteWarnpReply);
rpc GetLatestWarnings(GetLatestWarningsRequest) returns (GetLatestWarningsReply);
rpc GetUserWarnings(GetUserWarningsRequest) returns (GetUserWarningsReply); rpc GetUserWarnings(GetUserWarningsRequest) returns (GetUserWarningsReply);
rpc ClearWarning(ClearWarningRequest) returns (ClearWarningReply);
rpc SetWarnExpiry(SetWarnExpiryRequest) returns (SetWarnExpiryReply); rpc ForgiveWarning(ForgiveWarningRequest) returns (ForgiveWarningReply);
rpc DeleteWarning(ForgiveWarningRequest) returns (ForgiveWarningReply);
} }
message WarnSettingsRequest { message WarnSettingsRequest {
uint64 guildId = 1; uint64 guildId = 1;
@@ -19,12 +25,14 @@ message WarnSettingsRequest {
message WarnPunishment { message WarnPunishment {
int32 threshold = 1; int32 threshold = 1;
string action = 2; string action = 2;
int64 duration = 3; int32 duration = 3;
string role = 4;
} }
message WarnSettingsReply { message WarnSettingsReply {
repeated WarnPunishment punishments = 1; repeated WarnPunishment punishments = 1;
int32 expiryDays = 2; int32 expiryDays = 2;
bool deleteOnExpire = 3;
} }
message AddWarnpRequest { message AddWarnpRequest {
@@ -38,7 +46,7 @@ message AddWarnpReply {
message DeleteWarnpRequest { message DeleteWarnpRequest {
uint64 guildId = 1; uint64 guildId = 1;
int32 warnpIndex = 2; int32 threshold = 2;
} }
message DeleteWarnpReply { message DeleteWarnpReply {
@@ -47,37 +55,53 @@ message DeleteWarnpReply {
message GetUserWarningsRequest { message GetUserWarningsRequest {
uint64 guildId = 1; uint64 guildId = 1;
uint64 user_id = 2; string user = 2;
int32 page = 3;
} }
message GetUserWarningsReply { message GetUserWarningsReply {
repeated Warning warnings = 1; repeated Warning warnings = 1;
int32 totalCount = 2;
} }
message Warning { message Warning {
int32 id = 1; string id = 1;
string reason = 2; string reason = 2;
int64 timestamp = 3; int64 timestamp = 3;
int64 expiry_timestamp = 4; int64 weight = 4;
bool cleared = 5; bool forgiven = 5;
string clearedBy = 6; string forgivenBy = 6;
string user = 7;
uint64 userId = 8;
string moderator = 9;
} }
message ClearWarningRequest { message ForgiveWarningRequest {
uint64 guildId = 1; uint64 guildId = 1;
uint64 userId = 2; string warnId = 2;
optional int32 warnId = 3; string modName = 3;
} }
message ClearWarningReply { message ForgiveWarningReply {
bool success = 1; bool success = 1;
} }
message SetWarnExpiryRequest { message SetWarnExpiryRequest {
uint64 guildId = 1; uint64 guildId = 1;
int32 expiryDays = 2; int32 expiryDays = 2;
bool deleteOnExpire = 3;
} }
message SetWarnExpiryReply { message SetWarnExpiryReply {
bool success = 1; bool success = 1;
} }
message GetLatestWarningsRequest {
uint64 guildId = 1;
int32 page = 2;
}
message GetLatestWarningsReply {
repeated Warning warnings = 1;
int32 totalCount = 2;
}

View File

@@ -88,13 +88,6 @@ public static class DiscordUserExtensions
.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

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,7 +10,7 @@ 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)
{ {
@@ -20,7 +19,7 @@ public class AutoPublishService : IExecNoCommand, IReadyExecutor, INService
_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;
@@ -37,27 +36,37 @@ public class AutoPublishService : IExecNoCommand, IReadyExecutor, INService
}); });
} }
// 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,21 +75,21 @@ 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;

View File

@@ -207,6 +207,18 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, INService
.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()
{ {
GuildCount = guildIds.Keys.Count, GuildCount = guildIds.Keys.Count,

View File

@@ -139,7 +139,6 @@ public partial class Administration
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

@@ -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();
} }
@@ -287,7 +288,7 @@ 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)
{ {
@@ -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

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

View File

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

@@ -40,12 +40,17 @@ public static class WaifuExtensions
.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
? null
: x.Claimer.Username
+ (x.Claimer.Discriminator != "0000" ? "#" + x.Claimer.Discriminator : ""),
WaifuName = x.Waifu.Username
+ (x.Waifu.Discriminator != "0000" ? "#" + x.Waifu.Discriminator : ""),
Price = x.Price Price = x.Price
}) })
.ToListAsyncEF(); .ToListAsyncEF();

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

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

@@ -27,6 +27,8 @@ public interface IQuoteService
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);
Task<Quote?> GetQuoteByIdAsync(ulong guildId, int quoteId); Task<Quote?> GetQuoteByIdAsync(ulong guildId, int quoteId);
@@ -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

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

@@ -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;
@@ -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.13</Version> <Version>5.1.15</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>

View File

@@ -2,21 +2,34 @@
using Grpc.Core; using Grpc.Core;
using NadekoBot.Db.Models; using NadekoBot.Db.Models;
using NadekoBot.Modules.NadekoExpressions; using NadekoBot.Modules.NadekoExpressions;
using NadekoBot.Modules.Utility;
namespace NadekoBot.GrpcApi; namespace NadekoBot.GrpcApi;
public class ExprsSvc : GrpcExprs.GrpcExprsBase, INService public class ExprsSvc : GrpcExprs.GrpcExprsBase, IGrpcSvc, INService
{ {
private readonly NadekoExpressionsService _svc; private readonly NadekoExpressionsService _svc;
private readonly IQuoteService _qs;
private readonly DiscordSocketClient _client;
public ExprsSvc(NadekoExpressionsService svc) public ExprsSvc(NadekoExpressionsService svc, IQuoteService qs, DiscordSocketClient client)
{ {
_svc = svc; _svc = svc;
_qs = qs;
_client = client;
} }
[GrpcApiPerm(GuildPerm.Administrator)] 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) 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; NadekoExpression expr;
if (!string.IsNullOrWhiteSpace(request.Expr.Id)) if (!string.IsNullOrWhiteSpace(request.Expr.Id))
{ {
@@ -45,7 +58,6 @@ public class ExprsSvc : GrpcExprs.GrpcExprsBase, INService
}; };
} }
[GrpcApiPerm(GuildPerm.Administrator)]
public override async Task<GetExprsReply> GetExprs(GetExprsRequest request, ServerCallContext context) public override async Task<GetExprsReply> GetExprs(GetExprsRequest request, ServerCallContext context)
{ {
var (exprs, totalCount) = await _svc.FindExpressionsAsync(request.GuildId, request.Query, request.Page); var (exprs, totalCount) = await _svc.FindExpressionsAsync(request.GuildId, request.Query, request.Page);
@@ -66,11 +78,75 @@ public class ExprsSvc : GrpcExprs.GrpcExprsBase, INService
return reply; return reply;
} }
[GrpcApiPerm(GuildPerm.Administrator)]
public override async Task<Empty> DeleteExpr(DeleteExprRequest request, ServerCallContext context) public override async Task<Empty> DeleteExpr(DeleteExprRequest request, ServerCallContext context)
{ {
await _svc.DeleteAsync(request.GuildId, new kwum(request.Id)); if (kwum.TryParse(request.Id, out var id))
await _svc.DeleteAsync(request.GuildId, id);
return new Empty(); 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

@@ -3,7 +3,7 @@ using GreetType = NadekoBot.Services.GreetType;
namespace NadekoBot.GrpcApi; namespace NadekoBot.GrpcApi;
public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, INService public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, IGrpcSvc, INService
{ {
private readonly GreetService _gs; private readonly GreetService _gs;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
@@ -14,11 +14,8 @@ public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, INService
_client = client; _client = client;
} }
public GreetSettings GetDefaultGreet(GreetType type) public ServerServiceDefinition Bind()
=> new GreetSettings() => GrpcGreet.BindService(this);
{
GreetType = type
};
private static GrpcGreetSettings ToConf(GreetSettings? conf) private static GrpcGreetSettings ToConf(GreetSettings? conf)
{ {
@@ -29,40 +26,37 @@ public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, INService
{ {
Message = conf.MessageText, Message = conf.MessageText,
Type = (GrpcGreetType)conf.GreetType, Type = (GrpcGreetType)conf.GreetType,
ChannelId = conf.ChannelId ?? 0, ChannelId = conf.ChannelId?.ToString() ?? string.Empty,
IsEnabled = conf.IsEnabled, IsEnabled = conf.IsEnabled,
}; };
} }
[GrpcApiPerm(GuildPerm.Administrator)] public override async Task<GrpcGreetSettings> GetGreetSettings(GetGreetRequest request, ServerCallContext context)
public override async Task<GetGreetReply> GetGreetSettings(GetGreetRequest request, ServerCallContext context)
{ {
var guildId = request.GuildId; var guildId = request.GuildId;
var greetConf = await _gs.GetGreetSettingsAsync(guildId, GreetType.Greet); var conf = await _gs.GetGreetSettingsAsync(guildId, (GreetType)request.Type);
var byeConf = await _gs.GetGreetSettingsAsync(guildId, GreetType.Bye);
var boostConf = await _gs.GetGreetSettingsAsync(guildId, GreetType.Boost);
var greetDmConf = await _gs.GetGreetSettingsAsync(guildId, GreetType.GreetDm);
// todo timer
return new GetGreetReply() return ToConf(conf);
{
Greet = ToConf(greetConf),
Bye = ToConf(byeConf),
Boost = ToConf(boostConf),
GreetDm = ToConf(greetDmConf)
};
} }
[GrpcApiPerm(GuildPerm.Administrator)]
public override async Task<UpdateGreetReply> UpdateGreet(UpdateGreetRequest request, ServerCallContext context) public override async Task<UpdateGreetReply> UpdateGreet(UpdateGreetRequest request, ServerCallContext context)
{ {
var gid = request.GuildId; var gid = request.GuildId;
var s = request.Settings; var s = request.Settings;
var msg = s.Message; var msg = s.Message;
var type = GetGreetType(s.Type);
await _gs.SetMessage(gid, GetGreetType(s.Type), msg); await _gs.SetMessage(gid, GetGreetType(s.Type), msg);
await _gs.SetGreet(gid, s.ChannelId, GetGreetType(s.Type), s.IsEnabled); 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() return new()
{ {
@@ -70,7 +64,6 @@ public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, INService
}; };
} }
[GrpcApiPerm(GuildPerm.Administrator)]
public override Task<TestGreetReply> TestGreet(TestGreetRequest request, ServerCallContext context) public override Task<TestGreetReply> TestGreet(TestGreetRequest request, ServerCallContext context)
=> TestGreet(request.GuildId, request.ChannelId, request.UserId, request.Type); => TestGreet(request.GuildId, request.ChannelId, request.UserId, request.Type);

View File

@@ -11,7 +11,7 @@ public static class GrpcApiExtensions
=> ulong.Parse(context.RequestHeaders.FirstOrDefault(x => x.Key == "userid")!.Value); => ulong.Parse(context.RequestHeaders.FirstOrDefault(x => x.Key == "userid")!.Value);
} }
public sealed class OtherSvc : GrpcOther.GrpcOtherBase, INService public sealed class OtherSvc : GrpcOther.GrpcOtherBase, IGrpcSvc, INService
{ {
private readonly IDiscordClient _client; private readonly IDiscordClient _client;
private readonly XpService _xp; private readonly XpService _xp;
@@ -19,6 +19,7 @@ public sealed class OtherSvc : GrpcOther.GrpcOtherBase, INService
private readonly WaifuService _waifus; private readonly WaifuService _waifus;
private readonly ICoordinator _coord; private readonly ICoordinator _coord;
private readonly IStatsService _stats; private readonly IStatsService _stats;
private readonly IBotCache _cache;
public OtherSvc( public OtherSvc(
DiscordSocketClient client, DiscordSocketClient client,
@@ -26,7 +27,8 @@ public sealed class OtherSvc : GrpcOther.GrpcOtherBase, INService
ICurrencyService cur, ICurrencyService cur,
WaifuService waifus, WaifuService waifus,
ICoordinator coord, ICoordinator coord,
IStatsService stats) IStatsService stats,
IBotCache cache)
{ {
_client = client; _client = client;
_xp = xp; _xp = xp;
@@ -34,35 +36,41 @@ public sealed class OtherSvc : GrpcOther.GrpcOtherBase, INService
_waifus = waifus; _waifus = waifus;
_coord = coord; _coord = coord;
_stats = stats; _stats = stats;
_cache = cache;
} }
public override async Task<GetGuildsReply> GetGuilds(Empty request, ServerCallContext context) public ServerServiceDefinition Bind()
=> GrpcOther.BindService(this);
[GrpcNoAuthRequired]
public override async Task<BotOnGuildReply> BotOnGuild(BotOnGuildRequest request, ServerCallContext context)
{ {
var guilds = await _client.GetGuildsAsync(CacheMode.CacheOnly); var guild = await _client.GetGuildAsync(request.GuildId);
var reply = new GetGuildsReply(); var reply = new BotOnGuildReply
var userId = context.GetUserId();
var toReturn = new List<IGuild>();
foreach (var g in guilds)
{ {
var user = await g.GetUserAsync(userId, CacheMode.AllowDownload); Success = guild is not null
if (user.GuildPermissions.Has(GuildPermission.Administrator)) };
toReturn.Add(g);
} return reply;
}
reply.Guilds.AddRange(toReturn
.Select(x => new GuildReply() public override async Task<GetRolesReply> GetRoles(GetRolesRequest request, ServerCallContext context)
{ {
Id = x.Id, var g = await _client.GetGuildAsync(request.GuildId);
Name = x.Name, var roles = g?.Roles;
IconUrl = x.IconUrl 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; return reply;
} }
[GrpcApiPerm(GuildPerm.Administrator)]
public override async Task<GetTextChannelsReply> GetTextChannels( public override async Task<GetTextChannelsReply> GetTextChannels(
GetTextChannelsRequest request, GetTextChannelsRequest request,
ServerCallContext context) ServerCallContext context)
@@ -81,6 +89,35 @@ public sealed class OtherSvc : GrpcOther.GrpcOtherBase, INService
return reply; 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) public override async Task<CurrencyLbReply> GetCurrencyLb(GetLbRequest request, ServerCallContext context)
{ {
var users = await _cur.GetTopRichest(_client.CurrentUser.Id, request.Page, request.PerPage); var users = await _cur.GetTopRichest(_client.CurrentUser.Id, request.Page, request.PerPage);
@@ -103,9 +140,10 @@ public sealed class OtherSvc : GrpcOther.GrpcOtherBase, INService
return reply; return reply;
} }
[GrpcNoAuthRequired]
public override async Task<XpLbReply> GetXpLb(GetLbRequest request, ServerCallContext context) public override async Task<XpLbReply> GetXpLb(GetLbRequest request, ServerCallContext context)
{ {
var users = await _xp.GetUserXps(request.Page, request.PerPage); var users = await _xp.GetGlobalUserXps(request.Page);
var reply = new XpLbReply(); var reply = new XpLbReply();
@@ -127,6 +165,7 @@ public sealed class OtherSvc : GrpcOther.GrpcOtherBase, INService
return reply; return reply;
} }
[GrpcNoAuthRequired]
public override async Task<WaifuLbReply> GetWaifuLb(GetLbRequest request, ServerCallContext context) public override async Task<WaifuLbReply> GetWaifuLb(GetLbRequest request, ServerCallContext context)
{ {
var waifus = await _waifus.GetTopWaifusAtPage(request.Page, request.PerPage); var waifus = await _waifus.GetTopWaifusAtPage(request.Page, request.PerPage);
@@ -134,19 +173,23 @@ public sealed class OtherSvc : GrpcOther.GrpcOtherBase, INService
var reply = new WaifuLbReply(); var reply = new WaifuLbReply();
reply.Entries.AddRange(waifus.Select(x => new WaifuLbEntry() reply.Entries.AddRange(waifus.Select(x => new WaifuLbEntry()
{ {
ClaimedBy = x.Claimer ?? string.Empty, ClaimedBy = x.ClaimerName ?? string.Empty,
IsMutual = x.Claimer == x.Affinity, IsMutual = x.ClaimerName == x.Affinity,
Value = x.Price, Value = x.Price,
User = x.Username, User = x.WaifuName,
})); }));
return reply; return reply;
} }
public override Task<GetShardStatusesReply> GetShardStatuses(Empty request, ServerCallContext context) [GrpcNoAuthRequired]
public override async Task<GetShardStatusesReply> GetShardStatuses(Empty request, ServerCallContext context)
{ {
var reply = new GetShardStatusesReply(); var reply = new GetShardStatusesReply();
// todo cache await _cache.GetOrAddAsync<List<ShardStatus>>("coord:statuses",
() => Task.FromResult(_coord.GetAllShardStatuses().ToList())!,
TimeSpan.FromMinutes(1));
var shards = _coord.GetAllShardStatuses(); var shards = _coord.GetAllShardStatuses();
reply.Shards.AddRange(shards.Select(x => new ShardStatusReply() reply.Shards.AddRange(shards.Select(x => new ShardStatusReply()
@@ -157,10 +200,10 @@ public sealed class OtherSvc : GrpcOther.GrpcOtherBase, INService
LastUpdate = Timestamp.FromDateTime(x.LastUpdate), LastUpdate = Timestamp.FromDateTime(x.LastUpdate),
})); }));
return Task.FromResult(reply);
return reply;
} }
[GrpcApiPerm(GuildPerm.Administrator)]
public override async Task<GetServerInfoReply> GetServerInfo(ServerInfoRequest request, ServerCallContext context) public override async Task<GetServerInfoReply> GetServerInfo(ServerInfoRequest request, ServerCallContext context)
{ {
var info = await _stats.GetGuildInfoAsync(request.GuildId); var info = await _stats.GetGuildInfoAsync(request.GuildId);

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

@@ -9,22 +9,16 @@ public class GrpcApiService : INService, IReadyExecutor
private Server? _app; private Server? _app;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly OtherSvc _other; private readonly IEnumerable<IGrpcSvc> _svcs;
private readonly ExprsSvc _exprs;
private readonly GreetByeSvc _greet;
private readonly IBotCredsProvider _creds; private readonly IBotCredsProvider _creds;
public GrpcApiService( public GrpcApiService(
DiscordSocketClient client, DiscordSocketClient client,
OtherSvc other, IEnumerable<IGrpcSvc> svcs,
ExprsSvc exprs,
GreetByeSvc greet,
IBotCredsProvider creds) IBotCredsProvider creds)
{ {
_client = client; _client = client;
_other = other; _svcs = svcs;
_exprs = exprs;
_greet = greet;
_creds = creds; _creds = creds;
} }
@@ -39,28 +33,40 @@ public class GrpcApiService : INService, IReadyExecutor
var host = creds.GrpcApi.Host; var host = creds.GrpcApi.Host;
var port = creds.GrpcApi.Port + _client.ShardId; var port = creds.GrpcApi.Port + _client.ShardId;
var interceptor = new PermsInterceptor(_client); var interceptor = new GrpcApiPermsInterceptor(_client);
_app = new Server() var serverCreds = ServerCredentials.Insecure;
{
Services = if (creds.GrpcApi is
{ {
GrpcOther.BindService(_other).Intercept(interceptor), CertPrivateKey: not null and not "",
GrpcExprs.BindService(_exprs).Intercept(interceptor), CertChain: not null and not ""
GrpcGreet.BindService(_greet).Intercept(interceptor), } cert)
}, {
serverCreds = new SslServerCredentials(
new[] { new KeyCertificatePair(cert.CertChain, cert.CertPrivateKey) });
}
_app = new()
{
Ports = Ports =
{ {
new(host, port, ServerCredentials.Insecure), new(host, port, serverCreds),
} }
}; };
foreach (var svc in _svcs)
{
_app.Services.Add(svc.Bind().Intercept(interceptor));
}
_app.Start(); _app.Start();
Log.Information("Grpc Api Server started on port {Host}:{Port}", host, port); Log.Information("Grpc Api Server started on port {Host}:{Port}", host, port);
} }
catch catch (Exception ex)
{ {
Log.Error(ex, "Error starting Grpc Api Server");
_app?.ShutdownAsync().GetAwaiter().GetResult(); _app?.ShutdownAsync().GetAwaiter().GetResult();
} }

View File

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

View File

@@ -1,67 +0,0 @@
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace NadekoBot.GrpcApi;
public sealed partial class PermsInterceptor : Interceptor
{
private readonly DiscordSocketClient _client;
public PermsInterceptor(DiscordSocketClient client)
{
_client = client;
Log.Information("interceptor created");
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
try
{
Log.Information("Starting receiving call. Type/Method: {Type} / {Method}",
MethodType.Unary,
context.Method);
// get metadata
var metadata = context
.RequestHeaders
.ToDictionary(x => x.Key, x => x.Value);
var method = context.Method[(context.Method.LastIndexOf('/') + 1)..];
if (perms.TryGetValue(method, out var perm))
{
Log.Information("Required permission for {Method} is {Perm}",
method,
perm);
var userId = ulong.Parse(metadata["userid"]);
var guildId = ulong.Parse(metadata["guildid"]);
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"));
}
else
{
Log.Information("No permission required for {Method}", method);
}
return await continuation(request, context);
}
catch (Exception ex)
{
Log.Error(ex, "Error thrown by {ContextMethod}", context.Method);
throw;
}
}
}

View File

@@ -6,7 +6,7 @@ namespace NadekoBot.Common;
public sealed class Creds : IBotCreds public sealed class Creds : IBotCreds
{ {
[Comment("""DO NOT CHANGE""")] [Comment("""DO NOT CHANGE""")]
public int Version { get; set; } = 12; 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; }
@@ -292,8 +292,8 @@ public sealed class Creds : IBotCreds
public sealed record GrpcApiConfig public sealed record GrpcApiConfig
{ {
public bool Enabled { get; set; } = false; public bool Enabled { get; set; } = false;
public string CertPath { get; set; } = string.Empty; public string CertChain { get; set; } = string.Empty;
public string CertPassword { get; set; } = string.Empty; public string CertPrivateKey { get; set; } = string.Empty;
public string Host { get; set; } = "localhost"; public string Host { get; set; } = "localhost";
public int Port { get; set; } = 43120; public int Port { get; set; } = 43120;
} }

View File

@@ -140,9 +140,9 @@ public sealed class BotCredsProvider : IBotCredsProvider
creds.BotCache = BotCacheImplemenation.Memory; creds.BotCache = BotCacheImplemenation.Memory;
} }
if (creds.Version < 12) if (creds.Version < 13)
{ {
creds.Version = 12; creds.Version = 13;
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds)); File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
} }
} }

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

@@ -69,6 +69,7 @@ public abstract record SmartEmbedTextBase : SmartText
=> !string.IsNullOrWhiteSpace(Title) => !string.IsNullOrWhiteSpace(Title)
|| !string.IsNullOrWhiteSpace(Description) || !string.IsNullOrWhiteSpace(Description)
|| !string.IsNullOrWhiteSpace(Url) || !string.IsNullOrWhiteSpace(Url)
|| !string.IsNullOrWhiteSpace(Author?.Name)
|| !string.IsNullOrWhiteSpace(Thumbnail) || !string.IsNullOrWhiteSpace(Thumbnail)
|| !string.IsNullOrWhiteSpace(Image) || !string.IsNullOrWhiteSpace(Image)
|| (Footer is not null || (Footer is not null

View File

@@ -52,4 +52,14 @@ public class StoopidTime
Time = ts Time = ts
}; };
} }
public static implicit operator TimeSpan(StoopidTime st)
=> st.Time;
public static implicit operator StoopidTime(TimeSpan ts)
=> new()
{
Input = ts.ToString(),
Time = ts
};
} }

View File

@@ -365,7 +365,6 @@ quoteshow:
- qushow - qushow
quotesearch: quotesearch:
- quotesearch - quotesearch
- qs
- qse - qse
- qsearch - qsearch
quoteid: quoteid:

View File

@@ -1,5 +1,4 @@
# DO NOT CHANGE # DO NOT CHANGE
version: 1 version: 1
# List of medusae automatically loaded at startup # List of medusae automatically loaded at startup
loaded: loaded: []
- ngrpc