mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 01:38:27 -04:00
Compare commits
36 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
b34fd6da4e | ||
|
c9287dc166 | ||
|
7885106266 | ||
|
8efdd3dffe | ||
|
fb9a7964df | ||
|
1396d9d55a | ||
|
e7ddcebeab | ||
|
9d3a386f32 | ||
|
83c9c372e4 | ||
|
4bb4209c92 | ||
|
744018802f | ||
|
470bb9657f | ||
|
2fb4bb2ea4 | ||
|
43dd37c4f1 | ||
|
5fac500dcf | ||
|
fd25f5bf45 | ||
|
9d3e80eb32 | ||
|
42cbb7f626 | ||
|
4d175477f5 | ||
|
643987c41f | ||
|
03396642a4 | ||
|
3fd5f0c97a | ||
|
5d78f29329 | ||
|
2a98aceae6 | ||
|
5c933b676d | ||
|
2e4de7723e | ||
|
a8e00a19ba | ||
|
8acf6b1194 | ||
|
11d9db99ff | ||
|
c66e0fb6b7 | ||
|
1517a35ef7 | ||
|
c5179979d7 | ||
|
6b14c04e37 | ||
|
4ec3eb7855 | ||
|
4752c4b7cd | ||
|
dfec2f589e |
52
CHANGELOG.md
52
CHANGELOG.md
@@ -1,12 +1,60 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
||||||
|
|
||||||
## [4.2.9] - 25.06.202
|
## [4.2.15] - 12.07.2022
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `.nh*ntai` nsfw command
|
||||||
|
- Xp Freezes may have been fixed
|
||||||
|
- `data/images.yml` should once again support local file paths
|
||||||
|
- Fixed multiword aliases
|
||||||
|
|
||||||
|
## [4.2.14] - 03.07.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `.log userwarned` (Logging user warnings)
|
||||||
|
- Claiming `.timely` will now show a button which you can click to set a reminder
|
||||||
|
- Added `%server.icon%` placeholder
|
||||||
|
- Added `warn` punishment action for protection commands (it won't work with `.warnp`)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `.log userbanned` will now have a ban reason
|
||||||
|
- When `.die` is used, bot will try to update it's status to `Invisible`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed elipsis character issue with aliases/quotes. You should now be able to set an elipsis to be an alias of `.quoteprint`
|
||||||
|
|
||||||
|
## [4.2.13] - 30.06.2022
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `.cash` bank interaction not being ephemeral anymore
|
||||||
|
|
||||||
|
## [4.2.12] - 30.06.2022
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `.trivia --pokemon` showing incorrect pokemons
|
||||||
|
|
||||||
|
## [4.2.11] - 29.06.2022
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `.draw` command
|
||||||
|
|
||||||
|
## [4.2.10] - 29.06.2022
|
||||||
|
|
||||||
|
- Fixed currency generation working only once
|
||||||
|
|
||||||
|
## [4.2.9] - 25.06.2022
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
- Fixed `creds_example.yml` misssing from output directory
|
- Fixed `creds_example.yml` misssing from output directory
|
||||||
|
|
||||||
## [4.2.8] - 24.06.2022
|
## [4.2.8] - 24.06.2022
|
||||||
|
@@ -31,7 +31,8 @@
|
|||||||

|

|
||||||
- Click on **`DOWNLOAD`** at the lower right
|
- Click on **`DOWNLOAD`** at the lower right
|
||||||

|

|
||||||
- Click on **`Install`** next to **`Redis`**.
|
- Click on **`Install`** next to **`Redis`**.
|
||||||
|
- **(Note: Redis is optional unless you are are using the bot on 2000+ servers)**
|
||||||
- Note: If Redis fails to install, install Redis manually here: [Redis Installer](https://github.com/MicrosoftArchive/redis/releases/tag/win-3.0.504) Download and run the **`.msi`** file.
|
- Note: If Redis fails to install, install Redis manually here: [Redis Installer](https://github.com/MicrosoftArchive/redis/releases/tag/win-3.0.504) Download and run the **`.msi`** file.
|
||||||
- If you will use the music module, click on **`Install`** next to **`FFMPEG`** and **`Youtube-DL`**.
|
- If you will use the music module, click on **`Install`** next to **`FFMPEG`** and **`Youtube-DL`**.
|
||||||
- If any dependencies fail to install, you can temporarily disable your Windows Defender/AV until you install them. If you don't want to, then read [the last section of this guide](#Manual-Prerequisite-Installation).
|
- If any dependencies fail to install, you can temporarily disable your Windows Defender/AV until you install them. If you don't want to, then read [the last section of this guide](#Manual-Prerequisite-Installation).
|
||||||
|
@@ -64,7 +64,9 @@ public class CmdAttribute : System.Attribute
|
|||||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
{
|
{
|
||||||
// #if DEBUG
|
// #if DEBUG
|
||||||
// SpinWait.SpinUntil(() => Debugger.IsAttached);
|
// if (!Debugger.IsAttached)
|
||||||
|
// Debugger.Launch();
|
||||||
|
// // SpinWait.SpinUntil(() => Debugger.IsAttached);
|
||||||
// #endif
|
// #endif
|
||||||
context.RegisterPostInitializationOutput(static ctx => ctx.AddSource(
|
context.RegisterPostInitializationOutput(static ctx => ctx.AddSource(
|
||||||
"CmdAttribute.g.cs",
|
"CmdAttribute.g.cs",
|
||||||
@@ -157,7 +159,9 @@ public class CmdAttribute : System.Attribute
|
|||||||
.Distinct();
|
.Distinct();
|
||||||
|
|
||||||
var methodModels = methods
|
var methodModels = methods
|
||||||
.Select(x => MethodDeclarationToMethodModel(compilation, x!));
|
.Select(x => MethodDeclarationToMethodModel(compilation, x!))
|
||||||
|
.Where(static x => x is not null)
|
||||||
|
.Cast<MethodModel>();
|
||||||
|
|
||||||
var groups = methodModels
|
var groups = methodModels
|
||||||
.GroupBy(static x => $"{x.Namespace}.{string.Join(".", x.Classes)}");
|
.GroupBy(static x => $"{x.Namespace}.{string.Join(".", x.Classes)}");
|
||||||
@@ -177,7 +181,7 @@ public class CmdAttribute : System.Attribute
|
|||||||
var model = new FileModel(
|
var model = new FileModel(
|
||||||
methods: elems,
|
methods: elems,
|
||||||
ns: elems[0].Namespace,
|
ns: elems[0].Namespace,
|
||||||
classHierarchy: elems[0].Classes
|
classHierarchy: elems![0].Classes
|
||||||
);
|
);
|
||||||
|
|
||||||
models.Add(model);
|
models.Add(model);
|
||||||
@@ -187,11 +191,21 @@ public class CmdAttribute : System.Attribute
|
|||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MethodModel MethodDeclarationToMethodModel(Compilation comp, MethodDeclarationSyntax decl)
|
private static MethodModel? MethodDeclarationToMethodModel(Compilation comp, MethodDeclarationSyntax decl)
|
||||||
{
|
{
|
||||||
// SpinWait.SpinUntil(static () => Debugger.IsAttached);
|
// SpinWait.SpinUntil(static () => Debugger.IsAttached);
|
||||||
|
|
||||||
var semanticModel = comp.GetSemanticModel(decl.SyntaxTree);
|
SemanticModel semanticModel;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
semanticModel = comp.GetSemanticModel(decl.SyntaxTree);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// for some reason this method can throw "Not part of this compilation" argument exception
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var methodModel = new MethodModel(
|
var methodModel = new MethodModel(
|
||||||
@params: decl.ParameterList.Parameters
|
@params: decl.ParameterList.Parameters
|
||||||
.Where(p => p.Type is not null)
|
.Where(p => p.Type is not null)
|
||||||
|
@@ -70,6 +70,7 @@ public sealed class Bot
|
|||||||
: GatewayIntents.AllUnprivileged,
|
: GatewayIntents.AllUnprivileged,
|
||||||
LogGatewayIntentWarnings = false,
|
LogGatewayIntentWarnings = false,
|
||||||
FormatUsersInBidirectionalUnicode = false,
|
FormatUsersInBidirectionalUnicode = false,
|
||||||
|
DefaultRetryMode = RetryMode.AlwaysRetry ^ RetryMode.RetryRatelimit
|
||||||
});
|
});
|
||||||
|
|
||||||
_commandService = new(new()
|
_commandService = new(new()
|
||||||
@@ -260,6 +261,7 @@ public sealed class Bot
|
|||||||
Client.JoinedGuild += Client_JoinedGuild;
|
Client.JoinedGuild += Client_JoinedGuild;
|
||||||
Client.LeftGuild += Client_LeftGuild;
|
Client.LeftGuild += Client_LeftGuild;
|
||||||
|
|
||||||
|
// _ = Client.SetStatusAsync(UserStatus.Online);
|
||||||
Log.Information("Shard {ShardId} logged in", Client.ShardId);
|
Log.Information("Shard {ShardId} logged in", Client.ShardId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -27,5 +27,6 @@ public enum LogType
|
|||||||
UserPresence,
|
UserPresence,
|
||||||
VoicePresence,
|
VoicePresence,
|
||||||
VoicePresenceTts,
|
VoicePresenceTts,
|
||||||
UserMuted
|
UserMuted,
|
||||||
|
UserWarned,
|
||||||
}
|
}
|
@@ -67,13 +67,13 @@ public abstract class NadekoModule : ModuleBase
|
|||||||
|
|
||||||
// localized replies
|
// localized replies
|
||||||
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
|
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
|
||||||
=> SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
|
=> SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter);
|
||||||
|
|
||||||
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
|
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
|
||||||
=> SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
|
=> SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter);
|
||||||
|
|
||||||
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
|
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
|
||||||
=> SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
|
=> SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter);
|
||||||
|
|
||||||
public async Task<bool> PromptUserConfirmAsync(IEmbedBuilder embed)
|
public async Task<bool> PromptUserConfirmAsync(IEmbedBuilder embed)
|
||||||
{
|
{
|
||||||
|
@@ -57,6 +57,6 @@ public sealed class QueueRunner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask Enqueue(Func<Task> action)
|
public ValueTask EnqueueAsync(Func<Task> action)
|
||||||
=> _channel.Writer.WriteAsync(action);
|
=> _channel.Writer.WriteAsync(action);
|
||||||
}
|
}
|
@@ -55,6 +55,7 @@ public class ReplacementBuilder
|
|||||||
_reps.TryAdd("%server%", () => g is null ? "DM" : g.Name);
|
_reps.TryAdd("%server%", () => g is null ? "DM" : g.Name);
|
||||||
_reps.TryAdd("%server.id%", () => g is null ? "DM" : g.Id.ToString());
|
_reps.TryAdd("%server.id%", () => g is null ? "DM" : g.Id.ToString());
|
||||||
_reps.TryAdd("%server.name%", () => g is null ? "DM" : g.Name);
|
_reps.TryAdd("%server.name%", () => g is null ? "DM" : g.Name);
|
||||||
|
_reps.TryAdd("%server.icon%", () => g is null ? null : g.IconUrl);
|
||||||
_reps.TryAdd("%server.members%", () => g is { } sg ? sg.MemberCount.ToString() : "?");
|
_reps.TryAdd("%server.members%", () => g is { } sg ? sg.MemberCount.ToString() : "?");
|
||||||
_reps.TryAdd("%server.boosters%", () => g.PremiumSubscriptionCount.ToString());
|
_reps.TryAdd("%server.boosters%", () => g.PremiumSubscriptionCount.ToString());
|
||||||
_reps.TryAdd("%server.boost_level%", () => ((int)g.PremiumTier).ToString());
|
_reps.TryAdd("%server.boost_level%", () => ((int)g.PremiumTier).ToString());
|
||||||
|
@@ -44,12 +44,12 @@ public static class QuoteExtensions
|
|||||||
var rngk = new NadekoRandom();
|
var rngk = new NadekoRandom();
|
||||||
return (await quotes.AsQueryable()
|
return (await quotes.AsQueryable()
|
||||||
.Where(q => q.GuildId == guildId
|
.Where(q => q.GuildId == guildId
|
||||||
&& q.Keyword == keyword
|
&& (keyword == null || q.Keyword == keyword)
|
||||||
&& EF.Functions.Like(q.Text.ToUpper(), $"%{text.ToUpper()}%")
|
&& (EF.Functions.Like(q.Text.ToUpper(), $"%{text.ToUpper()}%")
|
||||||
// && q.Text.Contains(text, StringComparison.OrdinalIgnoreCase)
|
|| EF.Functions.Like(q.AuthorName, text)))
|
||||||
)
|
.ToListAsync())
|
||||||
.ToListAsync()).OrderBy(_ => rngk.Next())
|
.OrderBy(_ => rngk.Next())
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RemoveAllByKeyword(this DbSet<Quote> quotes, ulong guildId, string keyword)
|
public static void RemoveAllByKeyword(this DbSet<Quote> quotes, ulong guildId, string keyword)
|
||||||
|
@@ -48,7 +48,8 @@ public enum PunishmentAction
|
|||||||
RemoveRoles,
|
RemoveRoles,
|
||||||
ChatMute,
|
ChatMute,
|
||||||
VoiceMute,
|
VoiceMute,
|
||||||
AddRole
|
AddRole,
|
||||||
|
Warn
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AntiSpamIgnore : DbEntity
|
public class AntiSpamIgnore : DbEntity
|
||||||
|
@@ -4,7 +4,7 @@ namespace NadekoBot.Services.Database.Models;
|
|||||||
public class LogSetting : DbEntity
|
public class LogSetting : DbEntity
|
||||||
{
|
{
|
||||||
public List<IgnoredLogItem> LogIgnores { get; set; } = new();
|
public List<IgnoredLogItem> LogIgnores { get; set; } = new();
|
||||||
|
|
||||||
public ulong GuildId { get; set; }
|
public ulong GuildId { get; set; }
|
||||||
public ulong? LogOtherId { get; set; }
|
public ulong? LogOtherId { get; set; }
|
||||||
public ulong? MessageUpdatedId { get; set; }
|
public ulong? MessageUpdatedId { get; set; }
|
||||||
@@ -29,4 +29,5 @@ public class LogSetting : DbEntity
|
|||||||
|
|
||||||
public ulong? LogVoicePresenceId { get; set; }
|
public ulong? LogVoicePresenceId { get; set; }
|
||||||
public ulong? LogVoicePresenceTTSId { get; set; }
|
public ulong? LogVoicePresenceTTSId { get; set; }
|
||||||
|
public ulong? LogWarnsId { get; set; }
|
||||||
}
|
}
|
3518
src/NadekoBot/Migrations/MySql/20220703194400_logwarns.Designer.cs
generated
Normal file
3518
src/NadekoBot/Migrations/MySql/20220703194400_logwarns.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
src/NadekoBot/Migrations/MySql/20220703194400_logwarns.cs
Normal file
25
src/NadekoBot/Migrations/MySql/20220703194400_logwarns.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations.Mysql
|
||||||
|
{
|
||||||
|
public partial class logwarns : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<ulong>(
|
||||||
|
name: "logwarnsid",
|
||||||
|
table: "logsettings",
|
||||||
|
type: "bigint unsigned",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "logwarnsid",
|
||||||
|
table: "logsettings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1406,6 +1406,10 @@ namespace NadekoBot.Migrations.Mysql
|
|||||||
.HasColumnType("bigint unsigned")
|
.HasColumnType("bigint unsigned")
|
||||||
.HasColumnName("logvoicepresencettsid");
|
.HasColumnName("logvoicepresencettsid");
|
||||||
|
|
||||||
|
b.Property<ulong?>("LogWarnsId")
|
||||||
|
.HasColumnType("bigint unsigned")
|
||||||
|
.HasColumnName("logwarnsid");
|
||||||
|
|
||||||
b.Property<ulong?>("MessageDeletedId")
|
b.Property<ulong?>("MessageDeletedId")
|
||||||
.HasColumnType("bigint unsigned")
|
.HasColumnType("bigint unsigned")
|
||||||
.HasColumnName("messagedeletedid");
|
.HasColumnName("messagedeletedid");
|
||||||
|
3660
src/NadekoBot/Migrations/Postgresql/20220703194412_logwarns.Designer.cs
generated
Normal file
3660
src/NadekoBot/Migrations/Postgresql/20220703194412_logwarns.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations.PostgreSql
|
||||||
|
{
|
||||||
|
public partial class logwarns : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "logwarnsid",
|
||||||
|
table: "logsettings",
|
||||||
|
type: "numeric(20,0)",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "logwarnsid",
|
||||||
|
table: "logsettings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1472,6 +1472,10 @@ namespace NadekoBot.Migrations.PostgreSql
|
|||||||
.HasColumnType("numeric(20,0)")
|
.HasColumnType("numeric(20,0)")
|
||||||
.HasColumnName("logvoicepresencettsid");
|
.HasColumnName("logvoicepresencettsid");
|
||||||
|
|
||||||
|
b.Property<decimal?>("LogWarnsId")
|
||||||
|
.HasColumnType("numeric(20,0)")
|
||||||
|
.HasColumnName("logwarnsid");
|
||||||
|
|
||||||
b.Property<decimal?>("MessageDeletedId")
|
b.Property<decimal?>("MessageDeletedId")
|
||||||
.HasColumnType("numeric(20,0)")
|
.HasColumnType("numeric(20,0)")
|
||||||
.HasColumnName("messagedeletedid");
|
.HasColumnName("messagedeletedid");
|
||||||
|
2825
src/NadekoBot/Migrations/Sqlite/20220703194348_logwarns.Designer.cs
generated
Normal file
2825
src/NadekoBot/Migrations/Sqlite/20220703194348_logwarns.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
src/NadekoBot/Migrations/Sqlite/20220703194348_logwarns.cs
Normal file
43
src/NadekoBot/Migrations/Sqlite/20220703194348_logwarns.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations
|
||||||
|
{
|
||||||
|
public partial class logwarns : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Name",
|
||||||
|
table: "StreamOnlineMessages",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<ulong>(
|
||||||
|
name: "LogWarnsId",
|
||||||
|
table: "LogSettings",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LogWarnsId",
|
||||||
|
table: "LogSettings");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Name",
|
||||||
|
table: "StreamOnlineMessages",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -278,7 +278,6 @@ namespace NadekoBot.Migrations
|
|||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int>("Type")
|
b.Property<int>("Type")
|
||||||
@@ -1100,6 +1099,9 @@ namespace NadekoBot.Migrations
|
|||||||
b.Property<ulong?>("LogVoicePresenceTTSId")
|
b.Property<ulong?>("LogVoicePresenceTTSId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<ulong?>("LogWarnsId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<ulong?>("MessageDeletedId")
|
b.Property<ulong?>("MessageDeletedId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
@@ -335,6 +335,8 @@ public partial class Administration
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await _client.SetStatusAsync(UserStatus.Invisible);
|
||||||
|
_ = _client.StopAsync();
|
||||||
await ReplyConfirmLocalizedAsync(strs.shutting_down);
|
await ReplyConfirmLocalizedAsync(strs.shutting_down);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
@@ -26,6 +26,7 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
|
|||||||
private readonly IMemoryCache _memoryCache;
|
private readonly IMemoryCache _memoryCache;
|
||||||
|
|
||||||
private readonly ConcurrentHashSet<ulong> _ignoreMessageIds = new();
|
private readonly ConcurrentHashSet<ulong> _ignoreMessageIds = new();
|
||||||
|
private readonly UserPunishService _punishService;
|
||||||
|
|
||||||
public LogCommandService(
|
public LogCommandService(
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
@@ -35,7 +36,8 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
|
|||||||
ProtectionService prot,
|
ProtectionService prot,
|
||||||
GuildTimezoneService tz,
|
GuildTimezoneService tz,
|
||||||
IMemoryCache memoryCache,
|
IMemoryCache memoryCache,
|
||||||
IEmbedBuilderService eb)
|
IEmbedBuilderService eb,
|
||||||
|
UserPunishService punishService)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
_memoryCache = memoryCache;
|
_memoryCache = memoryCache;
|
||||||
@@ -45,6 +47,8 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
|
|||||||
_mute = mute;
|
_mute = mute;
|
||||||
_prot = prot;
|
_prot = prot;
|
||||||
_tz = tz;
|
_tz = tz;
|
||||||
|
_punishService = punishService;
|
||||||
|
|
||||||
using (var uow = db.GetDbContext())
|
using (var uow = db.GetDbContext())
|
||||||
{
|
{
|
||||||
var guildIds = client.Guilds.Select(x => x.Id).ToList();
|
var guildIds = client.Guilds.Select(x => x.Id).ToList();
|
||||||
@@ -78,6 +82,8 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
|
|||||||
_mute.UserUnmuted += MuteCommands_UserUnmuted;
|
_mute.UserUnmuted += MuteCommands_UserUnmuted;
|
||||||
|
|
||||||
_prot.OnAntiProtectionTriggered += TriggeredAntiProtection;
|
_prot.OnAntiProtectionTriggered += TriggeredAntiProtection;
|
||||||
|
|
||||||
|
_punishService.OnUserWarned += PunishServiceOnOnUserWarned;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task OnReadyAsync()
|
public async Task OnReadyAsync()
|
||||||
@@ -183,6 +189,30 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
|
|||||||
GuildLogSettings.AddOrUpdate(guildId, _ => logSetting, (_, _) => logSetting);
|
GuildLogSettings.AddOrUpdate(guildId, _ => logSetting, (_, _) => logSetting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private async Task PunishServiceOnOnUserWarned(Warning arg)
|
||||||
|
{
|
||||||
|
if (!GuildLogSettings.TryGetValue(arg.GuildId, out var logSetting) || logSetting.LogWarnsId is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var g = _client.GetGuild(arg.GuildId);
|
||||||
|
|
||||||
|
ITextChannel? logChannel;
|
||||||
|
if ((logChannel = await TryGetLogChannel(g, logSetting, LogType.UserWarned)) is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var embed = _eb.Create()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithTitle($"⚠️ User Warned")
|
||||||
|
.WithDescription($"<@{arg.UserId}> | {arg.UserId}")
|
||||||
|
.AddField("Mod", arg.Moderator)
|
||||||
|
.AddField("Reason", string.IsNullOrWhiteSpace(arg.Reason) ? "-" : arg.Reason, true)
|
||||||
|
.WithFooter(CurrentTime(g));
|
||||||
|
|
||||||
|
await logChannel.EmbedAsync(embed);
|
||||||
|
}
|
||||||
|
|
||||||
private Task _client_UserUpdated(SocketUser before, SocketUser uAfter)
|
private Task _client_UserUpdated(SocketUser before, SocketUser uAfter)
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
@@ -296,6 +326,9 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
|
|||||||
channelId = logSetting.LogVoicePresenceTTSId =
|
channelId = logSetting.LogVoicePresenceTTSId =
|
||||||
logSetting.LogVoicePresenceTTSId is null ? cid : default;
|
logSetting.LogVoicePresenceTTSId is null ? cid : default;
|
||||||
break;
|
break;
|
||||||
|
case LogType.UserWarned:
|
||||||
|
channelId = logSetting.LogWarnsId = logSetting.LogWarnsId is null ? cid : default;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
uow.SaveChanges();
|
uow.SaveChanges();
|
||||||
@@ -945,11 +978,25 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
|
|||||||
ITextChannel? logChannel;
|
ITextChannel? logChannel;
|
||||||
if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserBanned)) == null)
|
if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserBanned)) == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
||||||
|
string? reason = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ban = await guild.GetBanAsync(usr);
|
||||||
|
reason = ban?.Reason;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
var embed = _eb.Create()
|
var embed = _eb.Create()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithTitle("🚫 " + GetText(logChannel.Guild, strs.user_banned))
|
.WithTitle("🚫 " + GetText(logChannel.Guild, strs.user_banned))
|
||||||
.WithDescription(usr.ToString()!)
|
.WithDescription(usr.ToString()!)
|
||||||
.AddField("Id", usr.Id.ToString())
|
.AddField("Id", usr.Id.ToString())
|
||||||
|
.AddField("Reason", string.IsNullOrWhiteSpace(reason) ? "-" : reason)
|
||||||
.WithFooter(CurrentTime(guild));
|
.WithFooter(CurrentTime(guild));
|
||||||
|
|
||||||
var avatarUrl = usr.GetAvatarUrl();
|
var avatarUrl = usr.GetAvatarUrl();
|
||||||
@@ -1130,6 +1177,9 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
|
|||||||
case LogType.UserMuted:
|
case LogType.UserMuted:
|
||||||
id = logSetting.UserMutedId;
|
id = logSetting.UserMutedId;
|
||||||
break;
|
break;
|
||||||
|
case LogType.UserWarned:
|
||||||
|
id = logSetting.LogWarnsId;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id is null or 0)
|
if (id is null or 0)
|
||||||
@@ -1200,6 +1250,9 @@ public sealed class LogCommandService : ILogCommandService, IReadyExecutor
|
|||||||
case LogType.VoicePresenceTts:
|
case LogType.VoicePresenceTts:
|
||||||
newLogSetting.LogVoicePresenceTTSId = null;
|
newLogSetting.LogVoicePresenceTTSId = null;
|
||||||
break;
|
break;
|
||||||
|
case LogType.UserWarned:
|
||||||
|
newLogSetting.LogWarnsId = null;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
GuildLogSettings.AddOrUpdate(guildId, newLogSetting, (_, _) => newLogSetting);
|
GuildLogSettings.AddOrUpdate(guildId, newLogSetting, (_, _) => newLogSetting);
|
||||||
|
@@ -341,7 +341,8 @@ public partial class Administration
|
|||||||
public async partial Task WarnPunish(int number, PunishmentAction punish, StoopidTime time = null)
|
public async partial Task WarnPunish(int number, PunishmentAction punish, StoopidTime time = null)
|
||||||
{
|
{
|
||||||
// this should never happen. Addrole has its own method with higher priority
|
// this should never happen. Addrole has its own method with higher priority
|
||||||
if (punish == PunishmentAction.AddRole)
|
// also disallow warn punishment for getting warned
|
||||||
|
if (punish is PunishmentAction.AddRole or PunishmentAction.Warn)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time);
|
var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time);
|
||||||
|
@@ -18,6 +18,8 @@ public class UserPunishService : INService, IReadyExecutor
|
|||||||
private readonly BotConfigService _bcs;
|
private readonly BotConfigService _bcs;
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
|
|
||||||
|
public event Func<Warning, Task> OnUserWarned = static delegate { return Task.CompletedTask; };
|
||||||
|
|
||||||
public UserPunishService(
|
public UserPunishService(
|
||||||
MuteService mute,
|
MuteService mute,
|
||||||
DbService db,
|
DbService db,
|
||||||
@@ -93,6 +95,8 @@ public class UserPunishService : INService, IReadyExecutor
|
|||||||
await uow.SaveChangesAsync();
|
await uow.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = OnUserWarned(warn);
|
||||||
|
|
||||||
var totalCount = previousCount + weight;
|
var totalCount = previousCount + weight;
|
||||||
|
|
||||||
var p = ps.Where(x => x.Count > previousCount && x.Count <= totalCount)
|
var p = ps.Where(x => x.Count > previousCount && x.Count <= totalCount)
|
||||||
@@ -185,6 +189,9 @@ public class UserPunishService : INService, IReadyExecutor
|
|||||||
guild.Id);
|
guild.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case PunishmentAction.Warn:
|
||||||
|
await Warn(guild, user.Id, mod, 1, reason);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -44,7 +44,7 @@ public partial class Gambling
|
|||||||
var currentCard = cards.Draw();
|
var currentCard = cards.Draw();
|
||||||
cardObjects.Add(currentCard);
|
cardObjects.Add(currentCard);
|
||||||
var cardName = currentCard.ToString().ToLowerInvariant().Replace(' ', '_');
|
var cardName = currentCard.ToString().ToLowerInvariant().Replace(' ', '_');
|
||||||
images.Add(Image.Load(await File.ReadAllBytesAsync($"data/images/cards/{cardName}.png")));
|
images.Add(Image.Load(await File.ReadAllBytesAsync($"data/images/cards/{cardName}.jpg")));
|
||||||
}
|
}
|
||||||
|
|
||||||
using var img = images.Merge();
|
using var img = images.Merge();
|
||||||
|
@@ -7,6 +7,7 @@ using NadekoBot.Modules.Utility.Patronage;
|
|||||||
using NadekoBot.Modules.Gambling.Bank;
|
using NadekoBot.Modules.Gambling.Bank;
|
||||||
using NadekoBot.Modules.Gambling.Common;
|
using NadekoBot.Modules.Gambling.Common;
|
||||||
using NadekoBot.Modules.Gambling.Services;
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
|
using NadekoBot.Modules.Utility.Services;
|
||||||
using NadekoBot.Services.Currency;
|
using NadekoBot.Services.Currency;
|
||||||
using NadekoBot.Services.Database.Models;
|
using NadekoBot.Services.Database.Models;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
@@ -44,6 +45,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
private readonly GamblingConfigService _configService;
|
private readonly GamblingConfigService _configService;
|
||||||
private readonly IBankService _bank;
|
private readonly IBankService _bank;
|
||||||
private readonly IPatronageService _ps;
|
private readonly IPatronageService _ps;
|
||||||
|
private readonly RemindService _remind;
|
||||||
|
|
||||||
private IUserMessage rdMsg;
|
private IUserMessage rdMsg;
|
||||||
|
|
||||||
@@ -54,7 +56,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
DownloadTracker tracker,
|
DownloadTracker tracker,
|
||||||
GamblingConfigService configService,
|
GamblingConfigService configService,
|
||||||
IBankService bank,
|
IBankService bank,
|
||||||
IPatronageService ps)
|
IPatronageService ps,
|
||||||
|
RemindService remind)
|
||||||
: base(configService)
|
: base(configService)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
@@ -62,6 +65,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
_client = client;
|
_client = client;
|
||||||
_bank = bank;
|
_bank = bank;
|
||||||
_ps = ps;
|
_ps = ps;
|
||||||
|
_remind = remind;
|
||||||
|
|
||||||
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
|
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
|
||||||
_enUsCulture.NumberDecimalDigits = 0;
|
_enUsCulture.NumberDecimalDigits = 0;
|
||||||
@@ -109,7 +113,40 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
Key = "timely:extra_percent",
|
Key = "timely:extra_percent",
|
||||||
PrettyName = "Timely"
|
PrettyName = "Timely"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public class RemindMeInteraction : NInteraction
|
||||||
|
{
|
||||||
|
public RemindMeInteraction(
|
||||||
|
[NotNull] DiscordSocketClient client,
|
||||||
|
ulong userId,
|
||||||
|
[NotNull] Func<SocketMessageComponent, Task> action)
|
||||||
|
: base(client, userId, action)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override NadekoInteractionData Data
|
||||||
|
=> new NadekoInteractionData(
|
||||||
|
Emote: Emoji.Parse("⏰"),
|
||||||
|
CustomId: "timely:remind_me",
|
||||||
|
Text: "Remind me"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Func<SocketMessageComponent, Task> RemindTimelyAction(DateTime when)
|
||||||
|
=> async smc =>
|
||||||
|
{
|
||||||
|
var tt = TimestampTag.FromDateTime(when, TimestampTagStyles.Relative);
|
||||||
|
|
||||||
|
await _remind.AddReminderAsync(ctx.User.Id,
|
||||||
|
ctx.User.Id,
|
||||||
|
ctx.Guild.Id,
|
||||||
|
true,
|
||||||
|
when,
|
||||||
|
GetText(strs.timely_time));
|
||||||
|
|
||||||
|
await smc.RespondConfirmAsync(_eb, GetText(strs.remind_timely(tt)), ephemeral: true);
|
||||||
|
};
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
public async partial Task Timely()
|
public async partial Task Timely()
|
||||||
{
|
{
|
||||||
@@ -135,7 +172,11 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
|
|
||||||
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
|
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
|
||||||
|
|
||||||
await ReplyConfirmLocalizedAsync(strs.timely(N(val), period));
|
var inter = new RemindMeInteraction(_client,
|
||||||
|
ctx.User.Id,
|
||||||
|
RemindTimelyAction(DateTime.UtcNow.Add(TimeSpan.FromHours(period))));
|
||||||
|
|
||||||
|
await ReplyConfirmLocalizedAsync(strs.timely(N(val), period), inter.GetInteraction());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@@ -16,7 +16,7 @@ namespace NadekoBot.Modules.Gambling.Services;
|
|||||||
public class PlantPickService : INService, IExecNoCommand
|
public class PlantPickService : INService, IExecNoCommand
|
||||||
{
|
{
|
||||||
//channelId/last generation
|
//channelId/last generation
|
||||||
public ConcurrentDictionary<ulong, DateTime> LastGenerations { get; } = new();
|
public ConcurrentDictionary<ulong, long> LastGenerations { get; } = new();
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
private readonly IBotStrings _strings;
|
private readonly IBotStrings _strings;
|
||||||
private readonly IImageCache _images;
|
private readonly IImageCache _images;
|
||||||
@@ -175,15 +175,15 @@ public class PlantPickService : INService, IExecNoCommand
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var config = _gss.Data;
|
var config = _gss.Data;
|
||||||
var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue);
|
var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue.ToBinary());
|
||||||
var rng = new NadekoRandom();
|
var rng = new NadekoRandom();
|
||||||
|
|
||||||
if (DateTime.UtcNow - TimeSpan.FromSeconds(config.Generation.GenCooldown)
|
if (DateTime.UtcNow - TimeSpan.FromSeconds(config.Generation.GenCooldown)
|
||||||
< lastGeneration) //recently generated in this channel, don't generate again
|
< DateTime.FromBinary(lastGeneration)) //recently generated in this channel, don't generate again
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var num = rng.Next(1, 101) + (config.Generation.Chance * 100);
|
var num = rng.Next(1, 101) + (config.Generation.Chance * 100);
|
||||||
if (num > 100 && LastGenerations.TryUpdate(channel.Id, DateTime.UtcNow, lastGeneration))
|
if (num > 100 && LastGenerations.TryUpdate(channel.Id, DateTime.UtcNow.ToBinary(), lastGeneration))
|
||||||
{
|
{
|
||||||
var dropAmount = config.Generation.MinAmount;
|
var dropAmount = config.Generation.MinAmount;
|
||||||
var dropAmountMax = config.Generation.MaxAmount;
|
var dropAmountMax = config.Generation.MaxAmount;
|
||||||
|
@@ -19,14 +19,14 @@ public class TriviaQuestionPool
|
|||||||
{
|
{
|
||||||
var pokes = await _cache.GetPokemonMapAsync();
|
var pokes = await _cache.GetPokemonMapAsync();
|
||||||
|
|
||||||
if (pokes is null or { Length: 0 })
|
if (pokes is null or { Count: 0 })
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
var num = _rng.Next(1, _maxPokemonId + 1);
|
var num = _rng.Next(1, _maxPokemonId + 1);
|
||||||
return new(new()
|
return new(new()
|
||||||
{
|
{
|
||||||
Question = "Who's That Pokémon?",
|
Question = "Who's That Pokémon?",
|
||||||
Answer = pokes[num].Name.ToTitleCase(),
|
Answer = pokes[num].ToTitleCase(),
|
||||||
Category = "Pokemon",
|
Category = "Pokemon",
|
||||||
ImageUrl = $@"https://nadeko.bot/images/pokemon/shadows/{num}.png",
|
ImageUrl = $@"https://nadeko.bot/images/pokemon/shadows/{num}.png",
|
||||||
AnswerImageUrl = $@"https://nadeko.bot/images/pokemon/real/{num}.png"
|
AnswerImageUrl = $@"https://nadeko.bot/images/pokemon/real/{num}.png"
|
||||||
|
9
src/NadekoBot/Modules/Nsfw/Nhentai/INhentaiService.cs
Normal file
9
src/NadekoBot/Modules/Nsfw/Nhentai/INhentaiService.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using NadekoBot.Modules.Searches.Common;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Nsfw;
|
||||||
|
|
||||||
|
public interface INhentaiService
|
||||||
|
{
|
||||||
|
Task<Gallery?> GetAsync(uint id);
|
||||||
|
Task<IReadOnlyList<uint>> GetIdsBySearchAsync(string search);
|
||||||
|
}
|
115
src/NadekoBot/Modules/Nsfw/Nhentai/NhentaiScraperService.cs
Normal file
115
src/NadekoBot/Modules/Nsfw/Nhentai/NhentaiScraperService.cs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
using AngleSharp.Html.Dom;
|
||||||
|
using AngleSharp.Html.Parser;
|
||||||
|
using NadekoBot.Modules.Searches.Common;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Nsfw;
|
||||||
|
|
||||||
|
public sealed class NhentaiScraperService : INhentaiService, INService
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
|
|
||||||
|
private static readonly HtmlParser _htmlParser = new(new()
|
||||||
|
{
|
||||||
|
IsScripting = false,
|
||||||
|
IsEmbedded = false,
|
||||||
|
IsSupportingProcessingInstructions = false,
|
||||||
|
IsKeepingSourceReferences = false,
|
||||||
|
IsNotSupportingFrames = true
|
||||||
|
});
|
||||||
|
|
||||||
|
public NhentaiScraperService(IHttpClientFactory httpFactory)
|
||||||
|
{
|
||||||
|
_httpFactory = httpFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpClient GetHttpClient()
|
||||||
|
{
|
||||||
|
var http = _httpFactory.CreateClient();
|
||||||
|
http.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36");
|
||||||
|
http.DefaultRequestHeaders.Add("Cookie", "cf_clearance=I5pR71P4wJkRBFTLFjBndI.GwfKwT.Gx06uS8XNmRJo-1657214595-0-150; csrftoken=WMWRLtsQtBVQYvYkbqXKJHI9T1JwWCdd3tNhoxHn7aHLUYHAqe60XFUKAoWsJtda");
|
||||||
|
return http;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Gallery?> GetAsync(uint id)
|
||||||
|
{
|
||||||
|
using var http = GetHttpClient();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var url = $"https://nhentai.net/g/{id}/";
|
||||||
|
var strRes = await http.GetStringAsync(url);
|
||||||
|
var doc = await _htmlParser.ParseDocumentAsync(strRes);
|
||||||
|
|
||||||
|
var title = doc.QuerySelector("#info .title")?.TextContent;
|
||||||
|
var fullTitle = doc.QuerySelector("meta[itemprop=\"name\"]")?.Attributes["content"]?.Value
|
||||||
|
?? title;
|
||||||
|
var thumb = (doc.QuerySelector("#cover a img") as IHtmlImageElement)?.Dataset["src"];
|
||||||
|
|
||||||
|
var tagsElem = doc.QuerySelector("#tags");
|
||||||
|
|
||||||
|
var pageCount = tagsElem?.QuerySelector("a.tag[href^=\"/search/?q=pages\"] span")?.TextContent;
|
||||||
|
var likes = doc.QuerySelector(".buttons .btn-disabled.btn.tooltip span span")?.TextContent?.Trim('(', ')');
|
||||||
|
var uploadedAt = (tagsElem?.QuerySelector(".tag-container .tags time.nobold") as IHtmlTimeElement)?.DateTime;
|
||||||
|
|
||||||
|
var tags = tagsElem?.QuerySelectorAll(".tag-container .tags > a.tag[href^=\"/tag\"]")
|
||||||
|
.Cast<IHtmlAnchorElement>()
|
||||||
|
.Select(x => new Tag()
|
||||||
|
{
|
||||||
|
Name = x.QuerySelector("span:first-child")?.TextContent,
|
||||||
|
Url = $"https://nhentai.net{x.PathName}"
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(fullTitle))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!int.TryParse(pageCount, out var pc))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!int.TryParse(likes, out var lc))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!DateTime.TryParse(uploadedAt, out var ua))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new Gallery(id,
|
||||||
|
url,
|
||||||
|
fullTitle,
|
||||||
|
title,
|
||||||
|
thumb,
|
||||||
|
pc,
|
||||||
|
lc,
|
||||||
|
ua,
|
||||||
|
tags);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException)
|
||||||
|
{
|
||||||
|
Log.Warning("Nhentai with id {NhentaiId} not found", id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<uint>> GetIdsBySearchAsync(string search)
|
||||||
|
{
|
||||||
|
using var http = GetHttpClient();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var url = $"https://nhentai.net/search/?q={Uri.EscapeDataString(search)}&sort=popular-today";
|
||||||
|
var strRes = await http.GetStringAsync(url);
|
||||||
|
var doc = await _htmlParser.ParseDocumentAsync(strRes);
|
||||||
|
|
||||||
|
var elems = doc.QuerySelectorAll(".container .gallery a")
|
||||||
|
.Cast<IHtmlAnchorElement>()
|
||||||
|
.Where(x => x.PathName.StartsWith("/g/"))
|
||||||
|
.Select(x => x.PathName[3..^1])
|
||||||
|
.Select(uint.Parse)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return elems;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException)
|
||||||
|
{
|
||||||
|
Log.Warning("Nhentai search for {NhentaiSearch} failed", search);
|
||||||
|
return Array.Empty<uint>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
|
|||||||
|
|
||||||
namespace NadekoBot.Modules.Nsfw;
|
namespace NadekoBot.Modules.Nsfw;
|
||||||
|
|
||||||
|
#if !GLOBAL_NADEKO
|
||||||
[NoPublicBot]
|
[NoPublicBot]
|
||||||
public partial class NSFW : NadekoModule<ISearchImagesService>
|
public partial class NSFW : NadekoModule<ISearchImagesService>
|
||||||
{
|
{
|
||||||
@@ -403,15 +404,19 @@ public partial class NSFW : NadekoModule<ISearchImagesService>
|
|||||||
.Join(" ");
|
.Join(" ");
|
||||||
|
|
||||||
var embed = _eb.Create()
|
var embed = _eb.Create()
|
||||||
.WithTitle(g.Title)
|
.WithTitle(g.Title)
|
||||||
.WithDescription(g.FullTitle)
|
.WithDescription(g.FullTitle)
|
||||||
.WithImageUrl(g.Thumbnail)
|
.WithImageUrl(g.Thumbnail)
|
||||||
.WithUrl(g.Url)
|
.WithUrl(g.Url)
|
||||||
.AddField(GetText(strs.favorites), g.Likes, true)
|
.AddField(GetText(strs.favorites), g.Likes, true)
|
||||||
.AddField(GetText(strs.pages), g.PageCount, true)
|
.AddField(GetText(strs.pages), g.PageCount, true)
|
||||||
.AddField(GetText(strs.tags), tagString, true)
|
.AddField(GetText(strs.tags),
|
||||||
.WithFooter(g.UploadedAt.ToString("f"))
|
string.IsNullOrWhiteSpace(tagString)
|
||||||
.WithOkColor();
|
? "?"
|
||||||
|
: tagString,
|
||||||
|
true)
|
||||||
|
.WithFooter(g.UploadedAt.ToString("f"))
|
||||||
|
.WithOkColor();
|
||||||
|
|
||||||
await ctx.Channel.EmbedAsync(embed);
|
await ctx.Channel.EmbedAsync(embed);
|
||||||
}
|
}
|
||||||
@@ -436,4 +441,5 @@ public partial class NSFW : NadekoModule<ISearchImagesService>
|
|||||||
.WithFooter(
|
.WithFooter(
|
||||||
$"{data.Rating} ({data.Provider}) | {string.Join(" | ", data.Tags.Where(x => !string.IsNullOrWhiteSpace(x)).Take(5))}"));
|
$"{data.Rating} ({data.Provider}) | {string.Join(" | ", data.Tags.Where(x => !string.IsNullOrWhiteSpace(x)).Take(5))}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
@@ -1,10 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
namespace NadekoBot.Modules.Nsfw;
|
|
||||||
|
|
||||||
public interface INsfwService
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NsfwService
|
|
||||||
{
|
|
||||||
}
|
|
@@ -1,21 +1,11 @@
|
|||||||
#nullable disable
|
#nullable disable warnings
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using NadekoBot.Modules.Nsfw.Common;
|
using NadekoBot.Modules.Nsfw.Common;
|
||||||
using NadekoBot.Modules.Searches.Common;
|
using NadekoBot.Modules.Searches.Common;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Nsfw;
|
namespace NadekoBot.Modules.Nsfw;
|
||||||
|
|
||||||
public record UrlReply
|
|
||||||
{
|
|
||||||
public string Error { get; init; }
|
|
||||||
public string Url { get; init; }
|
|
||||||
public string Rating { get; init; }
|
|
||||||
public string Provider { get; init; }
|
|
||||||
public List<string> Tags { get; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SearchImagesService : ISearchImagesService, INService
|
public class SearchImagesService : ISearchImagesService, INService
|
||||||
{
|
{
|
||||||
private ConcurrentDictionary<ulong, HashSet<string>> BlacklistedTags { get; }
|
private ConcurrentDictionary<ulong, HashSet<string>> BlacklistedTags { get; }
|
||||||
@@ -23,18 +13,22 @@ public class SearchImagesService : ISearchImagesService, INService
|
|||||||
public ConcurrentDictionary<ulong, Timer> AutoHentaiTimers { get; } = new();
|
public ConcurrentDictionary<ulong, Timer> AutoHentaiTimers { get; } = new();
|
||||||
public ConcurrentDictionary<ulong, Timer> AutoBoobTimers { get; } = new();
|
public ConcurrentDictionary<ulong, Timer> AutoBoobTimers { get; } = new();
|
||||||
public ConcurrentDictionary<ulong, Timer> AutoButtTimers { get; } = new();
|
public ConcurrentDictionary<ulong, Timer> AutoButtTimers { get; } = new();
|
||||||
|
|
||||||
private readonly Random _rng;
|
private readonly Random _rng;
|
||||||
private readonly SearchImageCacher _cache;
|
private readonly SearchImageCacher _cache;
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
|
private readonly INhentaiService _nh;
|
||||||
|
|
||||||
private readonly object _taglock = new();
|
private readonly object _taglock = new();
|
||||||
|
|
||||||
public SearchImagesService(
|
public SearchImagesService(
|
||||||
DbService db,
|
DbService db,
|
||||||
SearchImageCacher cacher,
|
SearchImageCacher cacher,
|
||||||
IHttpClientFactory httpFactory)
|
IHttpClientFactory httpFactory,
|
||||||
|
INhentaiService nh)
|
||||||
{
|
{
|
||||||
|
_nh = nh;
|
||||||
_db = db;
|
_db = db;
|
||||||
_rng = new NadekoRandom();
|
_rng = new NadekoRandom();
|
||||||
_cache = cacher;
|
_cache = cacher;
|
||||||
@@ -284,85 +278,18 @@ public class SearchImagesService : ISearchImagesService, INService
|
|||||||
|
|
||||||
#region Nhentai
|
#region Nhentai
|
||||||
|
|
||||||
private string GetNhentaiExtensionInternal(string s)
|
public Task<Gallery?> GetNhentaiByIdAsync(uint id)
|
||||||
=> s switch
|
=> _nh.GetAsync(id);
|
||||||
{
|
|
||||||
"j" => "jpg",
|
|
||||||
"p" => "png",
|
|
||||||
"g" => "gif",
|
|
||||||
_ => "jpg"
|
|
||||||
};
|
|
||||||
|
|
||||||
private Gallery ModelToGallery(NhentaiApiModel.Gallery model)
|
public async Task<Gallery?> GetNhentaiBySearchAsync(string search)
|
||||||
{
|
{
|
||||||
var thumbnail = $"https://t.nhentai.net/galleries/{model.MediaId}/thumb."
|
var ids = await _nh.GetIdsBySearchAsync(search);
|
||||||
+ GetNhentaiExtensionInternal(model.Images.Thumbnail.T);
|
|
||||||
|
|
||||||
var url = $"https://nhentai.net/g/{model.Id}";
|
if (ids.Count == 0)
|
||||||
return new(model.Id.ToString(),
|
|
||||||
url,
|
|
||||||
model.Title.English,
|
|
||||||
model.Title.Pretty,
|
|
||||||
thumbnail,
|
|
||||||
model.NumPages,
|
|
||||||
model.NumFavorites,
|
|
||||||
model.UploadDate.ToUnixTimestamp().UtcDateTime,
|
|
||||||
model.Tags.Map(x => new Tag
|
|
||||||
{
|
|
||||||
Name = x.Name,
|
|
||||||
Url = "https://nhentai.com/" + x.Url
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<NhentaiApiModel.Gallery> GetNhentaiByIdInternalAsync(uint id)
|
|
||||||
{
|
|
||||||
using var http = _httpFactory.CreateClient();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var res = await http.GetStringAsync("https://nhentai.net/api/gallery/" + id);
|
|
||||||
return JsonConvert.DeserializeObject<NhentaiApiModel.Gallery>(res);
|
|
||||||
}
|
|
||||||
catch (HttpRequestException)
|
|
||||||
{
|
|
||||||
Log.Warning("Nhentai with id {NhentaiId} not found", id);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
}
|
var id = ids[_rng.Next(0, ids.Count)];
|
||||||
|
return await _nh.GetAsync(id);
|
||||||
private async Task<NhentaiApiModel.Gallery[]> SearchNhentaiInternalAsync(string search)
|
|
||||||
{
|
|
||||||
using var http = _httpFactory.CreateClient();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var res = await http.GetStringAsync("https://nhentai.net/api/galleries/search?query=" + search);
|
|
||||||
return JsonConvert.DeserializeObject<NhentaiApiModel.SearchResult>(res).Result;
|
|
||||||
}
|
|
||||||
catch (HttpRequestException)
|
|
||||||
{
|
|
||||||
Log.Warning("Nhentai with search {NhentaiSearch} not found", search);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Gallery> GetNhentaiByIdAsync(uint id)
|
|
||||||
{
|
|
||||||
var model = await GetNhentaiByIdInternalAsync(id);
|
|
||||||
|
|
||||||
return ModelToGallery(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly string[] _bannedTags = { "loli", "lolicon", "shota", "shotacon", "cub" };
|
|
||||||
|
|
||||||
public async Task<Gallery> GetNhentaiBySearchAsync(string search)
|
|
||||||
{
|
|
||||||
var models = await SearchNhentaiInternalAsync(search);
|
|
||||||
|
|
||||||
models = models.Where(x => !x.Tags.Any(t => _bannedTags.Contains(t.Name))).ToArray();
|
|
||||||
|
|
||||||
if (models.Length == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return ModelToGallery(models[_rng.Next(0, models.Length)]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
10
src/NadekoBot/Modules/Nsfw/UrlReply.cs
Normal file
10
src/NadekoBot/Modules/Nsfw/UrlReply.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace NadekoBot.Modules.Nsfw;
|
||||||
|
|
||||||
|
public record UrlReply
|
||||||
|
{
|
||||||
|
public string Error { get; init; }
|
||||||
|
public string Url { get; init; }
|
||||||
|
public string Rating { get; init; }
|
||||||
|
public string Provider { get; init; }
|
||||||
|
public List<string> Tags { get; } = new();
|
||||||
|
}
|
@@ -50,7 +50,7 @@ public sealed class FilterService : IExecOnMessage
|
|||||||
new(configs.SelectMany(gc => gc.FilterLinksChannelIds.Select(fci => fci.ChannelId)));
|
new(configs.SelectMany(gc => gc.FilterLinksChannelIds.Select(fci => fci.ChannelId)));
|
||||||
|
|
||||||
var dict = configs.ToDictionary(gc => gc.GuildId,
|
var dict = configs.ToDictionary(gc => gc.GuildId,
|
||||||
gc => new ConcurrentHashSet<string>(gc.FilteredWords.Select(fw => fw.Word)));
|
gc => new ConcurrentHashSet<string>(gc.FilteredWords.Select(fw => fw.Word).Distinct()));
|
||||||
|
|
||||||
ServerFilteredWords = new(dict);
|
ServerFilteredWords = new(dict);
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@ public sealed class Tag
|
|||||||
|
|
||||||
public sealed class Gallery
|
public sealed class Gallery
|
||||||
{
|
{
|
||||||
public string Id { get; }
|
public uint Id { get; }
|
||||||
public string Url { get; }
|
public string Url { get; }
|
||||||
public string FullTitle { get; }
|
public string FullTitle { get; }
|
||||||
public string Title { get; }
|
public string Title { get; }
|
||||||
@@ -21,7 +21,7 @@ public sealed class Gallery
|
|||||||
|
|
||||||
|
|
||||||
public Gallery(
|
public Gallery(
|
||||||
string id,
|
uint id,
|
||||||
string url,
|
string url,
|
||||||
string fullTitle,
|
string fullTitle,
|
||||||
string title,
|
string title,
|
||||||
|
@@ -1,121 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Searches.Common;
|
|
||||||
|
|
||||||
public static class NhentaiApiModel
|
|
||||||
{
|
|
||||||
public class Title
|
|
||||||
{
|
|
||||||
[JsonProperty("english")]
|
|
||||||
public string English { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("japanese")]
|
|
||||||
public string Japanese { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("pretty")]
|
|
||||||
public string Pretty { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Page
|
|
||||||
{
|
|
||||||
[JsonProperty("t")]
|
|
||||||
public string T { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("w")]
|
|
||||||
public int W { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("h")]
|
|
||||||
public int H { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Cover
|
|
||||||
{
|
|
||||||
[JsonProperty("t")]
|
|
||||||
public string T { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("w")]
|
|
||||||
public int W { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("h")]
|
|
||||||
public int H { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Thumbnail
|
|
||||||
{
|
|
||||||
[JsonProperty("t")]
|
|
||||||
public string T { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("w")]
|
|
||||||
public int W { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("h")]
|
|
||||||
public int H { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Images
|
|
||||||
{
|
|
||||||
[JsonProperty("pages")]
|
|
||||||
public List<Page> Pages { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("cover")]
|
|
||||||
public Cover Cover { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("thumbnail")]
|
|
||||||
public Thumbnail Thumbnail { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Tag
|
|
||||||
{
|
|
||||||
[JsonProperty("id")]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("type")]
|
|
||||||
public string Type { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("name")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("url")]
|
|
||||||
public string Url { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("count")]
|
|
||||||
public int Count { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Gallery
|
|
||||||
{
|
|
||||||
[JsonProperty("id")]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("media_id")]
|
|
||||||
public string MediaId { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("title")]
|
|
||||||
public Title Title { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("images")]
|
|
||||||
public Images Images { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("scanlator")]
|
|
||||||
public string Scanlator { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("upload_date")]
|
|
||||||
public double UploadDate { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("tags")]
|
|
||||||
public Tag[] Tags { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("num_pages")]
|
|
||||||
public int NumPages { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("num_favorites")]
|
|
||||||
public int NumFavorites { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SearchResult
|
|
||||||
{
|
|
||||||
[JsonProperty("result")]
|
|
||||||
public Gallery[] Result { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@@ -27,7 +27,8 @@ public class CommandMapService : IInputTransformer, INService
|
|||||||
|
|
||||||
AliasMaps = new(configs.ToDictionary(x => x.GuildId,
|
AliasMaps = new(configs.ToDictionary(x => x.GuildId,
|
||||||
x => new ConcurrentDictionary<string, string>(x.CommandAliases.DistinctBy(ca => ca.Trigger)
|
x => new ConcurrentDictionary<string, string>(x.CommandAliases.DistinctBy(ca => ca.Trigger)
|
||||||
.ToDictionary(ca => ca.Trigger, ca => ca.Mapping))));
|
.ToDictionary(ca => ca.Trigger, ca => ca.Mapping),
|
||||||
|
StringComparer.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
_db = db;
|
_db = db;
|
||||||
}
|
}
|
||||||
@@ -53,37 +54,72 @@ public class CommandMapService : IInputTransformer, INService
|
|||||||
{
|
{
|
||||||
if (guild is null || string.IsNullOrWhiteSpace(input))
|
if (guild is null || string.IsNullOrWhiteSpace(input))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (AliasMaps.TryGetValue(guild.Id, out var maps))
|
if (AliasMaps.TryGetValue(guild.Id, out var maps))
|
||||||
{
|
{
|
||||||
var keys = maps.Keys.OrderByDescending(x => x.Length);
|
string newInput = null;
|
||||||
|
foreach (var (k, v) in maps)
|
||||||
foreach (var k in keys)
|
|
||||||
{
|
{
|
||||||
string newInput;
|
if (string.Equals(input, k, StringComparison.OrdinalIgnoreCase))
|
||||||
if (input.StartsWith(k + " ", StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
newInput = maps[k] + input.Substring(k.Length, input.Length - k.Length);
|
|
||||||
else if (input.Equals(k, StringComparison.InvariantCultureIgnoreCase))
|
|
||||||
newInput = maps[k];
|
|
||||||
else
|
|
||||||
continue;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var toDelete = await channel.SendConfirmAsync(_eb, $"{input} => {newInput}");
|
newInput = v;
|
||||||
_ = Task.Run(async () =>
|
}
|
||||||
{
|
else if (input.StartsWith(k + ' ', StringComparison.OrdinalIgnoreCase))
|
||||||
await Task.Delay(1500);
|
{
|
||||||
await toDelete.DeleteAsync(new()
|
newInput = v + ' ' + input[k.Length..];
|
||||||
{
|
|
||||||
RetryMode = RetryMode.AlwaysRetry
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch { }
|
|
||||||
|
|
||||||
return newInput;
|
if (newInput is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var toDelete = await channel.SendConfirmAsync(_eb, $"{input} => {newInput}");
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(1500);
|
||||||
|
await toDelete.DeleteAsync(new()
|
||||||
|
{
|
||||||
|
RetryMode = RetryMode.AlwaysRetry
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return newInput;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// var keys = maps.Keys.OrderByDescending(x => x.Length);
|
||||||
|
// foreach (var k in keys)
|
||||||
|
// {
|
||||||
|
// string newInput;
|
||||||
|
// if (input.StartsWith(k + " ", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
// newInput = maps[k] + input.Substring(k.Length, input.Length - k.Length);
|
||||||
|
// else if (input.Equals(k, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
// newInput = maps[k];
|
||||||
|
// else
|
||||||
|
// continue;
|
||||||
|
//
|
||||||
|
// try
|
||||||
|
// {
|
||||||
|
// var toDelete = await channel.SendConfirmAsync(_eb, $"{input} => {newInput}");
|
||||||
|
// _ = Task.Run(async () =>
|
||||||
|
// {
|
||||||
|
// await Task.Delay(1500);
|
||||||
|
// await toDelete.DeleteAsync(new()
|
||||||
|
// {
|
||||||
|
// RetryMode = RetryMode.AlwaysRetry
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// catch { }
|
||||||
|
//
|
||||||
|
// return newInput;
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@@ -35,6 +35,7 @@ public sealed class PatronageService
|
|||||||
= new($"quota:last_hourly_reset");
|
= new($"quota:last_hourly_reset");
|
||||||
|
|
||||||
private readonly IBotCache _cache;
|
private readonly IBotCache _cache;
|
||||||
|
private readonly IBotCredsProvider _creds;
|
||||||
|
|
||||||
public PatronageService(
|
public PatronageService(
|
||||||
PatronageConfig pConf,
|
PatronageConfig pConf,
|
||||||
@@ -42,7 +43,8 @@ public sealed class PatronageService
|
|||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
ISubscriptionHandler subsHandler,
|
ISubscriptionHandler subsHandler,
|
||||||
IEmbedBuilderService eb,
|
IEmbedBuilderService eb,
|
||||||
IBotCache cache)
|
IBotCache cache,
|
||||||
|
IBotCredsProvider creds)
|
||||||
{
|
{
|
||||||
_pConf = pConf;
|
_pConf = pConf;
|
||||||
_db = db;
|
_db = db;
|
||||||
@@ -50,6 +52,7 @@ public sealed class PatronageService
|
|||||||
_subsHandler = subsHandler;
|
_subsHandler = subsHandler;
|
||||||
_eb = eb;
|
_eb = eb;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
|
_creds = creds;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task OnReadyAsync()
|
public Task OnReadyAsync()
|
||||||
@@ -495,8 +498,8 @@ public sealed class PatronageService
|
|||||||
if (!confData.IsEnabled)
|
if (!confData.IsEnabled)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
// if (_creds.IsOwner(userId))
|
if (_creds.GetCreds().IsOwner(userId))
|
||||||
// return default;
|
return default;
|
||||||
|
|
||||||
// get user tier
|
// get user tier
|
||||||
var patron = await GetPatronAsync(userId);
|
var patron = await GetPatronAsync(userId);
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
#nullable disable
|
#nullable disable warnings
|
||||||
using NadekoBot.Common.Yml;
|
using NadekoBot.Common.Yml;
|
||||||
using NadekoBot.Db;
|
using NadekoBot.Db;
|
||||||
using NadekoBot.Services.Database.Models;
|
using NadekoBot.Services.Database.Models;
|
||||||
@@ -134,30 +134,40 @@ public partial class Utility
|
|||||||
.WithFooter(
|
.WithFooter(
|
||||||
GetText(strs.created_by($"{data.AuthorName} ({data.AuthorId})"))));
|
GetText(strs.created_by($"{data.AuthorName} ({data.AuthorId})"))));
|
||||||
|
|
||||||
[Cmd]
|
private async Task QuoteSearchinternalAsync(string? keyword, string textOrAuthor)
|
||||||
[RequireContext(ContextType.Guild)]
|
|
||||||
public async partial Task QuoteSearch(string keyword, [Leftover] string text)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(keyword) || string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(textOrAuthor))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
keyword = keyword.ToUpperInvariant();
|
keyword = keyword?.ToUpperInvariant();
|
||||||
|
|
||||||
Quote keywordquote;
|
Quote quote;
|
||||||
await using (var uow = _db.GetDbContext())
|
await using (var uow = _db.GetDbContext())
|
||||||
{
|
{
|
||||||
keywordquote = await uow.Quotes.SearchQuoteKeywordTextAsync(ctx.Guild.Id, keyword, text);
|
quote = await uow.Quotes.SearchQuoteKeywordTextAsync(ctx.Guild.Id, keyword, textOrAuthor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keywordquote is null)
|
if (quote is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await ctx.Channel.SendMessageAsync($"`#{keywordquote.Id}` 💬 "
|
await ctx.Channel.SendMessageAsync($"`#{quote.Id}` 💬 "
|
||||||
+ keyword.ToLowerInvariant()
|
+ quote.Keyword.ToLowerInvariant()
|
||||||
+ ": "
|
+ ": "
|
||||||
+ keywordquote.Text.SanitizeAllMentions());
|
+ quote.Text.SanitizeAllMentions());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[Priority(0)]
|
||||||
|
public partial Task QuoteSearch(string textOrAuthor)
|
||||||
|
=> QuoteSearchinternalAsync(null, textOrAuthor);
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[Priority(1)]
|
||||||
|
public partial Task QuoteSearch(string keyword, [Leftover] string textOrAuthor)
|
||||||
|
=> QuoteSearchinternalAsync(keyword, textOrAuthor);
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
public async partial Task QuoteId(int id)
|
public async partial Task QuoteId(int id)
|
||||||
|
@@ -178,4 +178,27 @@ public class RemindService : INService, IReadyExecutor
|
|||||||
public string What { get; set; }
|
public string What { get; set; }
|
||||||
public TimeSpan Time { get; set; }
|
public TimeSpan Time { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task AddReminderAsync(ulong userId,
|
||||||
|
ulong targetId,
|
||||||
|
ulong? guildId,
|
||||||
|
bool isPrivate,
|
||||||
|
DateTime time,
|
||||||
|
string message)
|
||||||
|
{
|
||||||
|
var rem = new Reminder
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
ChannelId = targetId,
|
||||||
|
ServerId = guildId ?? 0,
|
||||||
|
IsPrivate = isPrivate,
|
||||||
|
When = time,
|
||||||
|
Message = message,
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var ctx = _db.GetDbContext();
|
||||||
|
await ctx.Reminders
|
||||||
|
.AddAsync(rem);
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
}
|
}
|
@@ -221,7 +221,7 @@ public class StreamRoleService : IReadyExecutor, INService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask RescanUser(IGuildUser user, StreamRoleSettings setting, IRole addRole = null)
|
private async ValueTask RescanUser(IGuildUser user, StreamRoleSettings setting, IRole addRole = null)
|
||||||
=> await _queueRunner.Enqueue(() => RescanUserInternal(user, setting, addRole));
|
=> await _queueRunner.EnqueueAsync(() => RescanUserInternal(user, setting, addRole));
|
||||||
|
|
||||||
private async Task RescanUserInternal(IGuildUser user, StreamRoleSettings setting, IRole addRole = null)
|
private async Task RescanUserInternal(IGuildUser user, StreamRoleSettings setting, IRole addRole = null)
|
||||||
{
|
{
|
||||||
@@ -239,7 +239,7 @@ public class StreamRoleService : IReadyExecutor, INService
|
|||||||
&& setting.Blacklist.All(x => x.UserId != user.Id)
|
&& setting.Blacklist.All(x => x.UserId != user.Id)
|
||||||
&& user.RoleIds.Contains(setting.FromRoleId))
|
&& user.RoleIds.Contains(setting.FromRoleId))
|
||||||
{
|
{
|
||||||
await _queueRunner.Enqueue(async () =>
|
await _queueRunner.EnqueueAsync(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -277,7 +277,7 @@ public class StreamRoleService : IReadyExecutor, INService
|
|||||||
//check if user is in the addrole
|
//check if user is in the addrole
|
||||||
if (user.RoleIds.Contains(setting.AddRoleId))
|
if (user.RoleIds.Contains(setting.AddRoleId))
|
||||||
{
|
{
|
||||||
await _queueRunner.Enqueue(async () =>
|
await _queueRunner.EnqueueAsync(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
using LinqToDB;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NadekoBot.Common.ModuleBehaviors;
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
using NadekoBot.Db;
|
using NadekoBot.Db;
|
||||||
@@ -12,7 +13,7 @@ using SixLabors.ImageSharp.Drawing.Processing;
|
|||||||
using SixLabors.ImageSharp.Formats;
|
using SixLabors.ImageSharp.Formats;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
using StackExchange.Redis;
|
using System.Threading.Channels;
|
||||||
using Color = SixLabors.ImageSharp.Color;
|
using Color = SixLabors.ImageSharp.Color;
|
||||||
using Image = SixLabors.ImageSharp.Image;
|
using Image = SixLabors.ImageSharp.Image;
|
||||||
|
|
||||||
@@ -37,13 +38,16 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _excludedChannels;
|
private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _excludedChannels;
|
||||||
private readonly ConcurrentHashSet<ulong> _excludedServers;
|
private readonly ConcurrentHashSet<ulong> _excludedServers;
|
||||||
|
|
||||||
private readonly System.Collections.Concurrent.ConcurrentQueue<UserCacheItem> _addMessageXp = new();
|
|
||||||
private XpTemplate template;
|
private XpTemplate template;
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
|
|
||||||
private readonly TypedKey<bool> _xpTemplateReloadKey;
|
private readonly TypedKey<bool> _xpTemplateReloadKey;
|
||||||
private readonly IPatronageService _ps;
|
private readonly IPatronageService _ps;
|
||||||
private readonly IBotCache _c;
|
private readonly IBotCache _c;
|
||||||
|
|
||||||
|
|
||||||
|
private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 50);
|
||||||
|
private readonly Channel<UserXpGainData> _xpGainQueue = Channel.CreateUnbounded<UserXpGainData>();
|
||||||
|
|
||||||
public XpService(
|
public XpService(
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
@@ -122,147 +126,131 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
|
|
||||||
public async Task OnReadyAsync()
|
public async Task OnReadyAsync()
|
||||||
{
|
{
|
||||||
|
_ = Task.Run(() => _levelUpQueue.RunAsync());
|
||||||
|
|
||||||
using var timer = new PeriodicTimer(5.Seconds());
|
using var timer = new PeriodicTimer(5.Seconds());
|
||||||
while (await timer.WaitForNextTickAsync())
|
while (await timer.WaitForNextTickAsync())
|
||||||
{
|
{
|
||||||
await UpdateLoop();
|
await UpdateXp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateLoop()
|
public sealed class MiniGuildXpStats
|
||||||
|
{
|
||||||
|
public long Xp { get; set; }
|
||||||
|
public XpNotificationLocation NotifyOnLevelUp { get; set; }
|
||||||
|
public ulong GuildId { get; set; }
|
||||||
|
public ulong UserId { get; set; }
|
||||||
|
}
|
||||||
|
private async Task UpdateXp()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var toNotify =
|
var reader = _xpGainQueue.Reader;
|
||||||
new List<(IGuild Guild, IMessageChannel MessageChannel, IUser User, long Level,
|
|
||||||
XpNotificationLocation NotifyType, NotifOf NotifOf)>();
|
// sum up all gains into a single UserCacheItem
|
||||||
var roleRewards = new Dictionary<ulong, List<XpRoleReward>>();
|
var globalToAdd = new Dictionary<ulong, UserXpGainData>();
|
||||||
var curRewards = new Dictionary<ulong, List<XpCurrencyReward>>();
|
var guildToAdd = new Dictionary<ulong, Dictionary<ulong, UserXpGainData>>();
|
||||||
|
while (reader.TryRead(out var item))
|
||||||
var toAddTo = new List<UserCacheItem>();
|
|
||||||
while (_addMessageXp.TryDequeue(out var usr))
|
|
||||||
toAddTo.Add(usr);
|
|
||||||
|
|
||||||
var group = toAddTo.GroupBy(x => (GuildId: x.Guild.Id, x.User));
|
|
||||||
if (toAddTo.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await using (var uow = _db.GetDbContext())
|
|
||||||
{
|
{
|
||||||
foreach (var item in group)
|
// add global xp to these users
|
||||||
|
if (!globalToAdd.TryGetValue(item.User.Id, out var ci))
|
||||||
|
globalToAdd[item.User.Id] = item.Clone();
|
||||||
|
else
|
||||||
|
ci.XpAmount += item.XpAmount;
|
||||||
|
|
||||||
|
|
||||||
|
// ad guild xp in these guilds to these users
|
||||||
|
if (!guildToAdd.TryGetValue(item.Guild.Id, out var users))
|
||||||
|
users = guildToAdd[item.Guild.Id] = new();
|
||||||
|
|
||||||
|
if (!users.TryGetValue(item.User.Id, out ci))
|
||||||
|
users[item.User.Id] = item.Clone();
|
||||||
|
else
|
||||||
|
ci.XpAmount += item.XpAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var ctx = _db.GetDbContext();
|
||||||
|
await using var tran = await ctx.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
// update global user xp in batches
|
||||||
|
// group by xp amount and update the same amounts at the same time
|
||||||
|
var dus = new List<DiscordUser>(globalToAdd.Count);
|
||||||
|
foreach (var group in globalToAdd.GroupBy(x => x.Value.XpAmount, x => x.Key))
|
||||||
|
{
|
||||||
|
var items = await ctx.DiscordUser
|
||||||
|
.Where(x => group.Contains(x.UserId))
|
||||||
|
.UpdateWithOutputAsync(old => new()
|
||||||
|
{
|
||||||
|
TotalXp = old.TotalXp + group.Key
|
||||||
|
},
|
||||||
|
(_, n) => n);
|
||||||
|
|
||||||
|
dus.AddRange(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update guild user xp in batches
|
||||||
|
var gxps = new List<UserXpStats>(globalToAdd.Count);
|
||||||
|
foreach (var (guildId, toAdd) in guildToAdd)
|
||||||
|
{
|
||||||
|
foreach (var group in toAdd.GroupBy(x => x.Value.XpAmount, x => x.Key))
|
||||||
{
|
{
|
||||||
var xp = item.Sum(x => x.XpAmount);
|
var items = await ctx
|
||||||
|
.UserXpStats
|
||||||
var usr = uow.GetOrCreateUserXpStats(item.Key.GuildId, item.Key.User.Id);
|
.Where(x => x.GuildId == guildId)
|
||||||
var du = uow.GetOrCreateUser(item.Key.User);
|
.Where(x => group.Contains(x.UserId))
|
||||||
|
.UpdateWithOutputAsync(old => new()
|
||||||
var globalXp = du.TotalXp;
|
|
||||||
var oldGlobalLevelData = new LevelStats(globalXp);
|
|
||||||
var newGlobalLevelData = new LevelStats(globalXp + xp);
|
|
||||||
|
|
||||||
var oldGuildLevelData = new LevelStats(usr.Xp + usr.AwardedXp);
|
|
||||||
usr.Xp += xp;
|
|
||||||
du.TotalXp += xp;
|
|
||||||
if (du.Club is not null)
|
|
||||||
du.Club.Xp += xp;
|
|
||||||
var newGuildLevelData = new LevelStats(usr.Xp + usr.AwardedXp);
|
|
||||||
|
|
||||||
if (oldGlobalLevelData.Level < newGlobalLevelData.Level)
|
|
||||||
{
|
|
||||||
var first = item.First();
|
|
||||||
if (du.NotifyOnLevelUp != XpNotificationLocation.None)
|
|
||||||
{
|
|
||||||
toNotify.Add((first.Guild, first.Channel, first.User, newGlobalLevelData.Level,
|
|
||||||
du.NotifyOnLevelUp, NotifOf.Global));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldGuildLevelData.Level < newGuildLevelData.Level)
|
|
||||||
{
|
|
||||||
//send level up notification
|
|
||||||
var first = item.First();
|
|
||||||
if (usr.NotifyOnLevelUp != XpNotificationLocation.None)
|
|
||||||
{
|
|
||||||
toNotify.Add((first.Guild, first.Channel, first.User, newGuildLevelData.Level,
|
|
||||||
usr.NotifyOnLevelUp, NotifOf.Server));
|
|
||||||
}
|
|
||||||
|
|
||||||
//give role
|
|
||||||
if (!roleRewards.TryGetValue(usr.GuildId, out var rrews))
|
|
||||||
{
|
|
||||||
rrews = uow.XpSettingsFor(usr.GuildId).RoleRewards.ToList();
|
|
||||||
roleRewards.Add(usr.GuildId, rrews);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!curRewards.TryGetValue(usr.GuildId, out var crews))
|
|
||||||
{
|
|
||||||
crews = uow.XpSettingsFor(usr.GuildId).CurrencyRewards.ToList();
|
|
||||||
curRewards.Add(usr.GuildId, crews);
|
|
||||||
}
|
|
||||||
|
|
||||||
//loop through levels since last level up, so if a high amount of xp is gained, reward are still applied.
|
|
||||||
for (var i = oldGuildLevelData.Level + 1; i <= newGuildLevelData.Level; i++)
|
|
||||||
{
|
|
||||||
var rrew = rrews.FirstOrDefault(x => x.Level == i);
|
|
||||||
if (rrew is not null)
|
|
||||||
{
|
{
|
||||||
var role = first.User.Guild.GetRole(rrew.RoleId);
|
Xp = old.Xp + group.Key
|
||||||
if (role is not null)
|
},
|
||||||
{
|
(_, n) => n);
|
||||||
if (rrew.Remove)
|
|
||||||
_ = first.User.RemoveRoleAsync(role);
|
|
||||||
else
|
|
||||||
_ = first.User.AddRoleAsync(role);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//get currency reward for this level
|
gxps.AddRange(items);
|
||||||
var crew = crews.FirstOrDefault(x => x.Level == i);
|
}
|
||||||
if (crew is not null)
|
}
|
||||||
//give the user the reward if it exists
|
|
||||||
await _cs.AddAsync(item.Key.User.Id, crew.Amount, new("xp", "level-up"));
|
await tran.CommitAsync();
|
||||||
}
|
|
||||||
}
|
foreach (var du in dus)
|
||||||
|
{
|
||||||
|
var oldLevel = new LevelStats(du.TotalXp - globalToAdd[du.UserId].XpAmount);
|
||||||
|
var newLevel = new LevelStats(du.TotalXp);
|
||||||
|
|
||||||
|
if (oldLevel.Level != newLevel.Level)
|
||||||
|
{
|
||||||
|
var item = globalToAdd[du.UserId];
|
||||||
|
await _levelUpQueue.EnqueueAsync(
|
||||||
|
NotifyUser(item.Guild.Id,
|
||||||
|
item.Channel.Id,
|
||||||
|
du.UserId,
|
||||||
|
false,
|
||||||
|
oldLevel.Level,
|
||||||
|
newLevel.Level,
|
||||||
|
du.NotifyOnLevelUp));
|
||||||
}
|
}
|
||||||
|
|
||||||
uow.SaveChanges();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await toNotify.Select(async x =>
|
foreach (var du in gxps)
|
||||||
{
|
{
|
||||||
if (x.NotifOf == NotifOf.Server)
|
if (guildToAdd.TryGetValue(du.GuildId, out var users)
|
||||||
{
|
&& users.TryGetValue(du.UserId, out var xpGainData))
|
||||||
if (x.NotifyType == XpNotificationLocation.Dm)
|
{
|
||||||
{
|
var oldLevel = new LevelStats(du.Xp - xpGainData.XpAmount);
|
||||||
await x.User.SendConfirmAsync(_eb,
|
var newLevel = new LevelStats(du.Xp);
|
||||||
_strings.GetText(strs.level_up_dm(x.User.Mention,
|
|
||||||
Format.Bold(x.Level.ToString()),
|
|
||||||
Format.Bold(x.Guild.ToString() ?? "-")),
|
|
||||||
x.Guild.Id));
|
|
||||||
}
|
|
||||||
else if (x.MessageChannel is not null) // channel
|
|
||||||
{
|
|
||||||
await x.MessageChannel.SendConfirmAsync(_eb,
|
|
||||||
_strings.GetText(strs.level_up_channel(x.User.Mention,
|
|
||||||
Format.Bold(x.Level.ToString())),
|
|
||||||
x.Guild.Id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
IMessageChannel chan;
|
|
||||||
if (x.NotifyType == XpNotificationLocation.Dm)
|
|
||||||
chan = await x.User.CreateDMChannelAsync();
|
|
||||||
else // channel
|
|
||||||
chan = x.MessageChannel;
|
|
||||||
|
|
||||||
await chan.SendConfirmAsync(_eb,
|
if (oldLevel.Level < newLevel.Level)
|
||||||
_strings.GetText(strs.level_up_global(x.User.Mention,
|
{
|
||||||
Format.Bold(x.Level.ToString())),
|
await _levelUpQueue.EnqueueAsync(
|
||||||
x.Guild.Id));
|
NotifyUser(xpGainData.Guild.Id,
|
||||||
}
|
xpGainData.Channel.Id,
|
||||||
})
|
du.UserId,
|
||||||
.WhenAll();
|
true,
|
||||||
|
oldLevel.Level,
|
||||||
|
newLevel.Level,
|
||||||
|
du.NotifyOnLevelUp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -270,7 +258,115 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Func<Task> NotifyUser(
|
||||||
|
ulong guildId,
|
||||||
|
ulong channelId,
|
||||||
|
ulong userId,
|
||||||
|
bool isServer,
|
||||||
|
long oldLevel,
|
||||||
|
long newLevel,
|
||||||
|
XpNotificationLocation notifyLoc)
|
||||||
|
=> async () =>
|
||||||
|
{
|
||||||
|
if (isServer)
|
||||||
|
{
|
||||||
|
await HandleRewardsInternalAsync(guildId, userId, oldLevel, newLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
await HandleNotifyInternalAsync(guildId, channelId, userId, isServer, newLevel, notifyLoc);
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task HandleRewardsInternalAsync(ulong guildId, ulong userId, long oldLevel, long newLevel)
|
||||||
|
{
|
||||||
|
await using var ctx = _db.GetDbContext();
|
||||||
|
var rrews = ctx.XpSettingsFor(guildId).RoleRewards.ToList();
|
||||||
|
var crews = ctx.XpSettingsFor(guildId).CurrencyRewards.ToList();
|
||||||
|
|
||||||
|
//loop through levels since last level up, so if a high amount of xp is gained, reward are still applied.
|
||||||
|
for (var i = oldLevel + 1; i <= newLevel; i++)
|
||||||
|
{
|
||||||
|
var rrew = rrews.FirstOrDefault(x => x.Level == i);
|
||||||
|
if (rrew is not null)
|
||||||
|
{
|
||||||
|
var guild = _client.GetGuild(guildId);
|
||||||
|
var role = guild?.GetRole(rrew.RoleId);
|
||||||
|
var user = guild?.GetUser(userId);
|
||||||
|
|
||||||
|
if (role is not null && user is not null)
|
||||||
|
{
|
||||||
|
if (rrew.Remove)
|
||||||
|
_ = user.RemoveRoleAsync(role);
|
||||||
|
else
|
||||||
|
_ = user.AddRoleAsync(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//get currency reward for this level
|
||||||
|
var crew = crews.FirstOrDefault(x => x.Level == i);
|
||||||
|
if (crew is not null)
|
||||||
|
{
|
||||||
|
//give the user the reward if it exists
|
||||||
|
await _cs.AddAsync(userId, crew.Amount, new("xp", "level-up"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleNotifyInternalAsync(ulong guildId,
|
||||||
|
ulong channelId,
|
||||||
|
ulong userId,
|
||||||
|
bool isServer,
|
||||||
|
long newLevel,
|
||||||
|
XpNotificationLocation notifyLoc)
|
||||||
|
{
|
||||||
|
if (notifyLoc == XpNotificationLocation.None)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var guild = _client.GetGuild(guildId);
|
||||||
|
var user = guild?.GetUser(userId);
|
||||||
|
var ch = guild?.GetTextChannel(channelId);
|
||||||
|
|
||||||
|
if (guild is null || user is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (isServer)
|
||||||
|
{
|
||||||
|
if (notifyLoc == XpNotificationLocation.Dm)
|
||||||
|
{
|
||||||
|
await user.SendConfirmAsync(_eb,
|
||||||
|
_strings.GetText(strs.level_up_dm(user.Mention,
|
||||||
|
Format.Bold(newLevel.ToString()),
|
||||||
|
Format.Bold(guild.ToString() ?? "-")),
|
||||||
|
guild.Id));
|
||||||
|
}
|
||||||
|
else // channel
|
||||||
|
{
|
||||||
|
await ch.SendConfirmAsync(_eb,
|
||||||
|
_strings.GetText(strs.level_up_channel(user.Mention,
|
||||||
|
Format.Bold(newLevel.ToString())),
|
||||||
|
guild.Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // global level
|
||||||
|
{
|
||||||
|
var chan = notifyLoc switch
|
||||||
|
{
|
||||||
|
XpNotificationLocation.Dm => (IMessageChannel)await user.CreateDMChannelAsync(),
|
||||||
|
XpNotificationLocation.Channel => ch,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (chan is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await chan.SendConfirmAsync(_eb,
|
||||||
|
_strings.GetText(strs.level_up_global(user.Mention,
|
||||||
|
Format.Bold(newLevel.ToString())),
|
||||||
|
guild.Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private const string XP_TEMPLATE_PATH = "./data/xp_template.json";
|
private const string XP_TEMPLATE_PATH = "./data/xp_template.json";
|
||||||
|
|
||||||
private void InternalReloadXpTemplate()
|
private void InternalReloadXpTemplate()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -295,7 +391,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
{
|
{
|
||||||
Log.Warning("Loaded default xp_template.json values as the old one was version 0. "
|
Log.Warning("Loaded default xp_template.json values as the old one was version 0. "
|
||||||
+ "Old one was renamed to xp_template.json.old");
|
+ "Old one was renamed to xp_template.json.old");
|
||||||
File.WriteAllText("./data/xp_template.json.old", JsonConvert.SerializeObject(template, Formatting.Indented));
|
File.WriteAllText("./data/xp_template.json.old",
|
||||||
|
JsonConvert.SerializeObject(template, Formatting.Indented));
|
||||||
template = new();
|
template = new();
|
||||||
template.Version = 1;
|
template.Version = 1;
|
||||||
File.WriteAllText(XP_TEMPLATE_PATH, JsonConvert.SerializeObject(template, Formatting.Indented));
|
File.WriteAllText(XP_TEMPLATE_PATH, JsonConvert.SerializeObject(template, Formatting.Indented));
|
||||||
@@ -473,9 +570,11 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
if (after.VoiceChannel is not null && after.VoiceChannel != before.VoiceChannel)
|
if (after.VoiceChannel is not null && after.VoiceChannel != before.VoiceChannel)
|
||||||
await ScanChannelForVoiceXp(after.VoiceChannel);
|
await ScanChannelForVoiceXp(after.VoiceChannel);
|
||||||
else if (after.VoiceChannel is null)
|
else if (after.VoiceChannel is null)
|
||||||
|
{
|
||||||
// In this case, the user left the channel and the previous for loops didn't catch
|
// In this case, the user left the channel and the previous for loops didn't catch
|
||||||
// it because it wasn't in any new channel. So we need to get rid of it.
|
// it because it wasn't in any new channel. So we need to get rid of it.
|
||||||
await UserLeftVoiceChannel(user, before.VoiceChannel);
|
await UserLeftVoiceChannel(user, before.VoiceChannel);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -546,7 +645,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
|
|
||||||
if (actualXp > 0)
|
if (actualXp > 0)
|
||||||
{
|
{
|
||||||
_addMessageXp.Enqueue(new()
|
await _xpGainQueue.Writer.WriteAsync(new()
|
||||||
{
|
{
|
||||||
Guild = channel.Guild,
|
Guild = channel.Guild,
|
||||||
User = user,
|
User = user,
|
||||||
@@ -593,7 +692,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
if (!await SetUserRewardedAsync(user.Id))
|
if (!await SetUserRewardedAsync(user.Id))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_addMessageXp.Enqueue(new()
|
await _xpGainQueue.Writer.WriteAsync(new()
|
||||||
{
|
{
|
||||||
Guild = user.Guild,
|
Guild = user.Guild,
|
||||||
Channel = arg.Channel,
|
Channel = arg.Channel,
|
||||||
@@ -604,19 +703,19 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddXpDirectly(IGuildUser user, IMessageChannel channel, int amount)
|
// public void AddXpDirectly(IGuildUser user, IMessageChannel channel, int amount)
|
||||||
{
|
// {
|
||||||
if (amount <= 0)
|
// if (amount <= 0)
|
||||||
throw new ArgumentOutOfRangeException(nameof(amount));
|
// throw new ArgumentOutOfRangeException(nameof(amount));
|
||||||
|
//
|
||||||
_addMessageXp.Enqueue(new()
|
// _xpGainQueue.Writer.WriteAsync(new()
|
||||||
{
|
// {
|
||||||
Guild = user.Guild,
|
// Guild = user.Guild,
|
||||||
Channel = channel,
|
// Channel = channel,
|
||||||
User = user,
|
// User = user,
|
||||||
XpAmount = amount
|
// XpAmount = amount
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
public void AddXp(ulong userId, ulong guildId, int amount)
|
public void AddXp(ulong userId, ulong guildId, int amount)
|
||||||
{
|
{
|
||||||
@@ -649,7 +748,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
|
|
||||||
private static TypedKey<bool> GetUserRewKey(ulong userId)
|
private static TypedKey<bool> GetUserRewKey(ulong userId)
|
||||||
=> new($"xp:user_gain:{userId}");
|
=> new($"xp:user_gain:{userId}");
|
||||||
|
|
||||||
private async Task<bool> SetUserRewardedAsync(ulong userId)
|
private async Task<bool> SetUserRewardedAsync(ulong userId)
|
||||||
=> await _c.AddAsync(GetUserRewKey(userId),
|
=> await _c.AddAsync(GetUserRewKey(userId),
|
||||||
true,
|
true,
|
||||||
@@ -829,8 +928,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
|
|
||||||
return font;
|
return font;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (template.User.GlobalLevel.Show)
|
if (template.User.GlobalLevel.Show)
|
||||||
{
|
{
|
||||||
// up to 83 width
|
// up to 83 width
|
||||||
@@ -841,7 +940,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
FontStyle.Bold,
|
FontStyle.Bold,
|
||||||
stats.Global.Level.ToString(),
|
stats.Global.Level.ToString(),
|
||||||
75);
|
75);
|
||||||
|
|
||||||
img.Mutate(x =>
|
img.Mutate(x =>
|
||||||
{
|
{
|
||||||
x.DrawText(stats.Global.Level.ToString(),
|
x.DrawText(stats.Global.Level.ToString(),
|
||||||
@@ -859,7 +958,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
FontStyle.Bold,
|
FontStyle.Bold,
|
||||||
stats.Guild.Level.ToString(),
|
stats.Guild.Level.ToString(),
|
||||||
75);
|
75);
|
||||||
|
|
||||||
img.Mutate(x =>
|
img.Mutate(x =>
|
||||||
{
|
{
|
||||||
x.DrawText(stats.Guild.Level.ToString(),
|
x.DrawText(stats.Guild.Level.ToString(),
|
||||||
@@ -936,14 +1035,14 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
if (template.User.GlobalRank.Show)
|
if (template.User.GlobalRank.Show)
|
||||||
{
|
{
|
||||||
var globalRankStr = stats.GlobalRanking.ToString();
|
var globalRankStr = stats.GlobalRanking.ToString();
|
||||||
|
|
||||||
var globalRankFont = GetTruncatedFont(
|
var globalRankFont = GetTruncatedFont(
|
||||||
_fonts.UniSans,
|
_fonts.UniSans,
|
||||||
template.User.GlobalRank.FontSize,
|
template.User.GlobalRank.FontSize,
|
||||||
FontStyle.Bold,
|
FontStyle.Bold,
|
||||||
globalRankStr,
|
globalRankStr,
|
||||||
68);
|
68);
|
||||||
|
|
||||||
img.Mutate(x => x.DrawText(globalRankStr,
|
img.Mutate(x => x.DrawText(globalRankStr,
|
||||||
globalRankFont,
|
globalRankFont,
|
||||||
template.User.GlobalRank.Color,
|
template.User.GlobalRank.Color,
|
||||||
@@ -953,20 +1052,20 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
if (template.User.GuildRank.Show)
|
if (template.User.GuildRank.Show)
|
||||||
{
|
{
|
||||||
var guildRankStr = stats.GuildRanking.ToString();
|
var guildRankStr = stats.GuildRanking.ToString();
|
||||||
|
|
||||||
var guildRankFont = GetTruncatedFont(
|
var guildRankFont = GetTruncatedFont(
|
||||||
_fonts.UniSans,
|
_fonts.UniSans,
|
||||||
template.User.GuildRank.FontSize,
|
template.User.GuildRank.FontSize,
|
||||||
FontStyle.Bold,
|
FontStyle.Bold,
|
||||||
guildRankStr,
|
guildRankStr,
|
||||||
43);
|
43);
|
||||||
|
|
||||||
img.Mutate(x => x.DrawText(guildRankStr,
|
img.Mutate(x => x.DrawText(guildRankStr,
|
||||||
guildRankFont,
|
guildRankFont,
|
||||||
template.User.GuildRank.Color,
|
template.User.GuildRank.Color,
|
||||||
new(template.User.GuildRank.Pos.X, template.User.GuildRank.Pos.Y)));
|
new(template.User.GuildRank.Pos.X, template.User.GuildRank.Pos.Y)));
|
||||||
}
|
}
|
||||||
|
|
||||||
//avatar
|
//avatar
|
||||||
if (stats.User.AvatarId is not null && template.User.Icon.Show)
|
if (stats.User.AvatarId is not null && template.User.Icon.Show)
|
||||||
{
|
{
|
||||||
@@ -1018,13 +1117,13 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
// #if GLOBAL_NADEKO
|
// #if GLOBAL_NADEKO
|
||||||
await DrawFrame(img, stats.User.UserId);
|
await DrawFrame(img, stats.User.UserId);
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
var outputSize = template.OutputSize;
|
var outputSize = template.OutputSize;
|
||||||
if (outputSize.X != img.Width || outputSize.Y != img.Height)
|
if (outputSize.X != img.Width || outputSize.Y != img.Height)
|
||||||
img.Mutate(x => x.Resize(template.OutputSize.X, template.OutputSize.Y));
|
img.Mutate(x => x.Resize(template.OutputSize.X, template.OutputSize.Y));
|
||||||
|
|
||||||
var output = ((Stream)await img.ToStreamAsync(imageFormat), imageFormat);
|
var output = ((Stream)await img.ToStreamAsync(imageFormat), imageFormat);
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@ using NadekoBot.Modules.Xp.Services;
|
|||||||
|
|
||||||
namespace NadekoBot.Modules.Xp;
|
namespace NadekoBot.Modules.Xp;
|
||||||
|
|
||||||
public class LevelStats
|
public readonly struct LevelStats
|
||||||
{
|
{
|
||||||
public long Level { get; }
|
public long Level { get; }
|
||||||
public long LevelXp { get; }
|
public long LevelXp { get; }
|
||||||
|
@@ -1,16 +1,13 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
using Cloneable;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Xp.Services;
|
namespace NadekoBot.Modules.Xp.Services;
|
||||||
|
|
||||||
public class UserCacheItem
|
[Cloneable]
|
||||||
|
public sealed partial class UserXpGainData : ICloneable<UserXpGainData>
|
||||||
{
|
{
|
||||||
public IGuildUser User { get; set; }
|
public IGuildUser User { get; set; }
|
||||||
public IGuild Guild { get; set; }
|
public IGuild Guild { get; set; }
|
||||||
public IMessageChannel Channel { get; set; }
|
public IMessageChannel Channel { get; set; }
|
||||||
public int XpAmount { get; set; }
|
public int XpAmount { get; set; }
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
=> User.GetHashCode();
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
=> obj is UserCacheItem uci && uci.User == User;
|
|
||||||
}
|
}
|
@@ -9,5 +9,5 @@ public interface ILocalDataCache
|
|||||||
Task<IReadOnlyDictionary<string, SearchPokemon>> GetPokemonsAsync();
|
Task<IReadOnlyDictionary<string, SearchPokemon>> GetPokemonsAsync();
|
||||||
Task<IReadOnlyDictionary<string, SearchPokemonAbility>> GetPokemonAbilitiesAsync();
|
Task<IReadOnlyDictionary<string, SearchPokemonAbility>> GetPokemonAbilitiesAsync();
|
||||||
Task<TriviaQuestionModel[]> GetTriviaQuestionsAsync();
|
Task<TriviaQuestionModel[]> GetTriviaQuestionsAsync();
|
||||||
Task<PokemonNameId[]> GetPokemonMapAsync();
|
Task<IReadOnlyDictionary<int, string>> GetPokemonMapAsync();
|
||||||
}
|
}
|
@@ -67,11 +67,37 @@ public sealed class LocalDataCache : ILocalDataCache, INService
|
|||||||
=> await GetOrCreateCachedDataAsync(_pokemonAbilitiesKey, POKEMON_ABILITIES_FILE);
|
=> await GetOrCreateCachedDataAsync(_pokemonAbilitiesKey, POKEMON_ABILITIES_FILE);
|
||||||
|
|
||||||
|
|
||||||
private static TypedKey<PokemonNameId[]> _pokeMapKey
|
private static TypedKey<IReadOnlyDictionary<int, string>> _pokeMapKey
|
||||||
= new("pokemon:ab_map");
|
= new("pokemon:ab_map2"); // 2 because ab_map was storing arrays
|
||||||
|
|
||||||
public async Task<PokemonNameId[]?> GetPokemonMapAsync()
|
public async Task<IReadOnlyDictionary<int, string>?> GetPokemonMapAsync()
|
||||||
=> await GetOrCreateCachedDataAsync(_pokeMapKey, POKEMON_MAP_PATH);
|
=> await _cache.GetOrAddAsync(_pokeMapKey,
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
var fileName = POKEMON_MAP_PATH;
|
||||||
|
if (!File.Exists(fileName))
|
||||||
|
{
|
||||||
|
Log.Warning($"{fileName} is missing. Relevant data can't be loaded");
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var stream = File.OpenRead(fileName);
|
||||||
|
var arr = await JsonSerializer.DeserializeAsync<PokemonNameId[]>(stream, _opts);
|
||||||
|
|
||||||
|
return (IReadOnlyDictionary<int, string>?)arr?.ToDictionary(x => x.Id, x => x.Name);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex,
|
||||||
|
"Error reading {FileName} file: {ErrorMessage}",
|
||||||
|
fileName,
|
||||||
|
ex.Message);
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
private static TypedKey<TriviaQuestionModel[]> _triviaKey
|
private static TypedKey<TriviaQuestionModel[]> _triviaKey
|
||||||
|
@@ -59,6 +59,11 @@ public sealed class ImageCache : IImageCache, INService
|
|||||||
GetImageKey(url),
|
GetImageKey(url),
|
||||||
async () =>
|
async () =>
|
||||||
{
|
{
|
||||||
|
if (url.IsFile)
|
||||||
|
{
|
||||||
|
return await File.ReadAllBytesAsync(url.LocalPath);
|
||||||
|
}
|
||||||
|
|
||||||
using var http = _httpFactory.CreateClient();
|
using var http = _httpFactory.CreateClient();
|
||||||
var bytes = await http.GetByteArrayAsync(url);
|
var bytes = await http.GetByteArrayAsync(url);
|
||||||
return bytes;
|
return bytes;
|
||||||
|
@@ -7,7 +7,7 @@ namespace NadekoBot.Services;
|
|||||||
|
|
||||||
public sealed class StatsService : IStatsService, IReadyExecutor, INService
|
public sealed class StatsService : IStatsService, IReadyExecutor, INService
|
||||||
{
|
{
|
||||||
public const string BOT_VERSION = "4.2.9";
|
public const string BOT_VERSION = "4.2.15";
|
||||||
|
|
||||||
public string Author
|
public string Author
|
||||||
=> "Kwoth#2452";
|
=> "Kwoth#2452";
|
||||||
|
@@ -58,7 +58,8 @@ public static class SocketMessageComponentExtensions
|
|||||||
bool ephemeral = false)
|
bool ephemeral = false)
|
||||||
=> smc.RespondAsync(plainText,
|
=> smc.RespondAsync(plainText,
|
||||||
embed: embed?.Build(),
|
embed: embed?.Build(),
|
||||||
embeds: embeds?.Map(x => x.Build()));
|
embeds: embeds?.Map(x => x.Build()),
|
||||||
|
ephemeral: ephemeral);
|
||||||
|
|
||||||
public static Task RespondAsync(
|
public static Task RespondAsync(
|
||||||
this SocketMessageComponent ch,
|
this SocketMessageComponent ch,
|
||||||
|
@@ -67,8 +67,7 @@ helpText: |-
|
|||||||
# List of modules and commands completely blocked on the bot
|
# List of modules and commands completely blocked on the bot
|
||||||
blocked:
|
blocked:
|
||||||
commands: []
|
commands: []
|
||||||
modules:
|
modules: []
|
||||||
- nsfw
|
|
||||||
# Which string will be used to recognize the commands
|
# Which string will be used to recognize the commands
|
||||||
prefix: .
|
prefix: .
|
||||||
# Toggles whether your bot will group greet/bye messages into a single message every 5 seconds.
|
# Toggles whether your bot will group greet/bye messages into a single message every 5 seconds.
|
||||||
|
@@ -601,9 +601,12 @@ quoteshow:
|
|||||||
args:
|
args:
|
||||||
- "123"
|
- "123"
|
||||||
quotesearch:
|
quotesearch:
|
||||||
desc: "Shows a random quote for a keyword that contains any text specified in the search."
|
desc: "Shows a random quote given a search query. Partially matches in several ways: 1) Only content of any quote, 2) only by author, 3) keyword and content, 3) or keyword and author"
|
||||||
args:
|
args:
|
||||||
- "keyword text"
|
- "\"find this long text\""
|
||||||
|
- "AuthorName"
|
||||||
|
- "keyword some text"
|
||||||
|
- "keyword AuthorName"
|
||||||
quoteid:
|
quoteid:
|
||||||
desc: "Displays the quote with the specified ID number. Quote ID numbers can be found by typing `{0}liqu [num]` where `[num]` is a number of a page which contains 15 quotes."
|
desc: "Displays the quote with the specified ID number. Quote ID numbers can be found by typing `{0}liqu [num]` where `[num]` is a number of a page which contains 15 quotes."
|
||||||
args:
|
args:
|
||||||
|
@@ -596,6 +596,7 @@
|
|||||||
"quote_deleted": "Quote #{0} deleted.",
|
"quote_deleted": "Quote #{0} deleted.",
|
||||||
"region": "Region",
|
"region": "Region",
|
||||||
"remind": "I will remind {0} to {1} in {2} `({3:d.M.yyyy.} at {4:HH:mm})`",
|
"remind": "I will remind {0} to {1} in {2} `({3:d.M.yyyy.} at {4:HH:mm})`",
|
||||||
|
"remind_timely": "I will remind you about your timely reward {0}",
|
||||||
"remind_invalid": "Not a valid remind format. Remind must have a target, timer and a reason. Check the command list.",
|
"remind_invalid": "Not a valid remind format. Remind must have a target, timer and a reason. Check the command list.",
|
||||||
"remind_too_long": "Remind time has exceeded maximum.",
|
"remind_too_long": "Remind time has exceeded maximum.",
|
||||||
"repeater_redundant_no": "Repeater **#{0}** won't post redundant messages anymore.",
|
"repeater_redundant_no": "Repeater **#{0}** won't post redundant messages anymore.",
|
||||||
@@ -883,6 +884,7 @@
|
|||||||
"timely_set": "Users will be able to claim {0} every {1}h",
|
"timely_set": "Users will be able to claim {0} every {1}h",
|
||||||
"timely_set_none": "Users will not be able to claim any timely currency.",
|
"timely_set_none": "Users will not be able to claim any timely currency.",
|
||||||
"timely_reset": "All users will be able to claim timely currency again.",
|
"timely_reset": "All users will be able to claim timely currency again.",
|
||||||
|
"timely_time": "It's time for your timely reward.",
|
||||||
"price": "Price",
|
"price": "Price",
|
||||||
"market_cap": "Market Cap",
|
"market_cap": "Market Cap",
|
||||||
"market_cap_dominance": "Dominance",
|
"market_cap_dominance": "Dominance",
|
||||||
|
Reference in New Issue
Block a user