mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 09:48:26 -04:00
Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3a5b482884 | ||
|
db66264bc6 | ||
|
ae1ddd82d0 | ||
|
8523abd6f1 | ||
|
e1892c4ff4 | ||
|
a50a7b3b0e | ||
|
9d2268a925 | ||
|
d77a86c08b | ||
|
d605f685cf | ||
|
bbc1fd28c2 | ||
|
cff8a258d0 | ||
|
1d760a548e | ||
|
25fa8a3852 | ||
|
ca13684c0d | ||
|
0ad6b741e7 | ||
|
4ce756d760 | ||
|
5f2813d3af | ||
|
1b7458529c | ||
|
9c9c8d7490 | ||
|
2700bfdce8 | ||
|
5498c5ce3f | ||
|
6f444a8da0 | ||
|
454c14eee1 | ||
|
30aa8e8186 | ||
|
2ca141810c | ||
|
9da8e4f1c1 | ||
|
ef471c32bb | ||
|
49d557caec | ||
|
4366f908f3 | ||
|
237e66495b | ||
|
15d4117d7f | ||
|
d7daa5f2af | ||
|
b08ff62406 | ||
|
9aa89d3be8 | ||
|
518f2e425e | ||
|
8fae6e621d | ||
|
30f3ae1ade | ||
|
ef4b1c8868 | ||
|
7f64d2661f | ||
|
ab93380d7c | ||
|
a6adf73ecf | ||
|
d9e52038ac | ||
|
be7ddc732b | ||
|
79b25c8a41 | ||
|
12fa209555 |
@@ -30,12 +30,17 @@ variables:
|
|||||||
build:
|
build:
|
||||||
stage: build
|
stage: build
|
||||||
script:
|
script:
|
||||||
- "dotnet publish -c Release -r linux-x64 --self-contained -o $LINUX_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
- |
|
||||||
- "dotnet publish -c Release -r linux-arm64 --self-contained -o $LINUX_ARM64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
VERSION_STRING=""
|
||||||
- "dotnet publish -c Release -r win-x64 --self-contained -o $WIN_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||||
- "dotnet publish -c Release -r win-arm64 --self-contained -o $WIN_ARM64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
VERSION_STRING="-p:Version=$CI_COMMIT_TAG"
|
||||||
- "dotnet publish -c Release -r osx-x64 --self-contained -o $MACOS_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
fi
|
||||||
- "dotnet publish -c Release -r osx-arm64 --self-contained -o $MACOS_ARM64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
- "dotnet publish -c Release -r linux-x64 --self-contained $VERSION_STRING -o $LINUX_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
||||||
|
- "dotnet publish -c Release -r linux-arm64 --self-contained $VERSION_STRING -o $LINUX_ARM64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
||||||
|
- "dotnet publish -c Release -r win-x64 --self-contained $VERSION_STRING -o $WIN_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
||||||
|
- "dotnet publish -c Release -r win-arm64 --self-contained $VERSION_STRING -o $WIN_ARM64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
||||||
|
- "dotnet publish -c Release -r osx-x64 --self-contained $VERSION_STRING -o $MACOS_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
||||||
|
- "dotnet publish -c Release -r osx-arm64 --self-contained $VERSION_STRING -o $MACOS_ARM64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- "$LINUX_X64_OUTPUT_DIR/"
|
- "$LINUX_X64_OUTPUT_DIR/"
|
||||||
|
93
CHANGELOG.md
93
CHANGELOG.md
@@ -2,6 +2,99 @@
|
|||||||
|
|
||||||
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
||||||
|
|
||||||
|
## [5.1.4] - 13.07.2024
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `.coins` command which lists top 10 cryptos ordered by marketcap
|
||||||
|
- Added Clubs rank in the leaderboard to `.clubinfo`
|
||||||
|
- Bot owners can now check other people's bank balance (Not server owners, only bot owner, the person who is hosting the bot)
|
||||||
|
- You can now send multiple waifu gifts at once to waifus. For example `.waifugift 3xRose @user` will give that user 3 roses
|
||||||
|
- The format is `<NUMBER>x<ITEM>`, no spaces
|
||||||
|
- Added `.boosttest` command
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated command strings to clarify `.say` and `.send` usages
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `.waifugift` help string
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Removed selfhost button from `.donate` command, no idea why it was there in the first place
|
||||||
|
|
||||||
|
## [5.1.3] - 06.07.2024
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `.quran` command, which will show the provided ayah in english and arabic, including recitation by Alafasy
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Replying to the bot's message in the channel where chatterbot is enabled will also trigger the ai response, as if you pinged the bot. This only works for chatterbot, but not for nadeko ai command prompts
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `.stickeradd` it now properly supports 300x300 image uploads.
|
||||||
|
- Bot should now trim the invalid characters from chatterbot usernames to avoid openai errors
|
||||||
|
- Fixed prompt triggering chatterbot responses twice
|
||||||
|
|
||||||
|
## [5.1.2] - 29.06.2024
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `.honeypot` not unbanning and not pruning messages
|
||||||
|
|
||||||
|
## [5.1.1] - 27.06.2024
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `.honeypot` command, which automatically softbans (ban and immediate unban) any user who posts in that channel.
|
||||||
|
- Useful to auto softban bots who spam every channel upon joining
|
||||||
|
- Users who run commands or expressions won't be softbanned.
|
||||||
|
- Users who have ban member permissions are also excluded.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed `.betdraw` not respecting maxbet
|
||||||
|
- Fixed `.xpshop` pagination for real this time?
|
||||||
|
|
||||||
|
## [5.1.0] - 25.06.2024
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `.prompt` command, Nadeko Ai Assistant
|
||||||
|
- You can send natural language questions, queries or execute commands. For example "@Nadeko how's the weather in paris" and it will return `.we Paris` and run it for you.
|
||||||
|
- In case the bot can't execute a command using your query, It will fall back to your chatter bot, in case you have it enabled in data/games.yml. (Cleverbot or chatgpt)
|
||||||
|
- (It's far from perfect so please don't ask the bot to do dangerous things like banning or pruning)
|
||||||
|
- Requires Patreon subscription, after which you'll be able to run it on global @Nadeko bot.
|
||||||
|
- Selfhosters: If you're selfhosting, you also will need to acquire the api key from <https://dashy.nadeko.bot/me> after pledging on patreon and put it in nadekoAiToken in creds.yml
|
||||||
|
- Added support for `gpt-4o` in `data/games.yml`
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Remind will now show a timestamp tag for durations
|
||||||
|
- Only `Gpt35Turbo` and `Gpt4o` are valid inputs in games.yml now
|
||||||
|
- `data/patron.yml` changed. It now has limits. The entire feature limit system has been reworked. Your previous settings will be reset
|
||||||
|
- A lot of updates to bot strings (thanks Ene)
|
||||||
|
- Improved cleanup command to delete a lot more data once cleanup is ran, not only guild configs (please don't use this command unless you have your database bakced up and you know 100% what you're doing)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed xp bg buy button not working, and possibly some other buttons too
|
||||||
|
- Fixed shopbuy %user% placeholders and updated help text
|
||||||
|
- All .feed overloads should now work"
|
||||||
|
- `.xpexclude` should will now work with forums too. If you exclude a forum you won't be able to gain xp in any of the threads.
|
||||||
|
- Fixed remind not showing correct time (thx cata)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Removed PoE related commands
|
||||||
|
- dev: Removed patron quota data from the database, it will now be stored in redis
|
||||||
|
|
||||||
## [5.0.8] - 21.05.2024
|
## [5.0.8] - 21.05.2024
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@@ -181,9 +181,9 @@ dotnet_naming_rule.private_readonly_field.symbols = private_readonly_field
|
|||||||
dotnet_naming_rule.private_readonly_field.style = begins_with_underscore
|
dotnet_naming_rule.private_readonly_field.style = begins_with_underscore
|
||||||
dotnet_naming_rule.private_readonly_field.severity = warning
|
dotnet_naming_rule.private_readonly_field.severity = warning
|
||||||
|
|
||||||
dotnet_naming_rule.private_field.symbols = private_field
|
# dotnet_naming_rule.private_field.symbols = private_field
|
||||||
dotnet_naming_rule.private_field.style = camel_case
|
# dotnet_naming_rule.private_field.style = camel_case
|
||||||
dotnet_naming_rule.private_field.severity = warning
|
# dotnet_naming_rule.private_field.severity = warning
|
||||||
|
|
||||||
dotnet_naming_rule.const_fields.symbols = const_fields
|
dotnet_naming_rule.const_fields.symbols = const_fields
|
||||||
dotnet_naming_rule.const_fields.style = all_upper
|
dotnet_naming_rule.const_fields.style = all_upper
|
||||||
|
@@ -93,7 +93,7 @@ public sealed class Bot : IBot
|
|||||||
private void AddServices()
|
private void AddServices()
|
||||||
{
|
{
|
||||||
var startingGuildIdList = GetCurrentGuildIds();
|
var startingGuildIdList = GetCurrentGuildIds();
|
||||||
var sw = Stopwatch.StartNew();
|
var startTime = Stopwatch.GetTimestamp();
|
||||||
var bot = Client.CurrentUser;
|
var bot = Client.CurrentUser;
|
||||||
|
|
||||||
using (var uow = _db.GetDbContext())
|
using (var uow = _db.GetDbContext())
|
||||||
@@ -161,8 +161,7 @@ public sealed class Bot : IBot
|
|||||||
LoadTypeReaders(a);
|
LoadTypeReaders(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
sw.Stop();
|
Log.Information("All services loaded in {ServiceLoadTime:F2}s", Stopwatch.GetElapsedTime(startTime) .TotalSeconds);
|
||||||
Log.Information("All services loaded in {ServiceLoadTime:F2}s", sw.Elapsed.TotalSeconds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadTypeReaders(Assembly assembly)
|
private void LoadTypeReaders(Assembly assembly)
|
||||||
@@ -259,7 +258,7 @@ public sealed class Bot : IBot
|
|||||||
if (ShardId == 0)
|
if (ShardId == 0)
|
||||||
await _db.SetupAsync();
|
await _db.SetupAsync();
|
||||||
|
|
||||||
var sw = Stopwatch.StartNew();
|
var startTime = Stopwatch.GetTimestamp();
|
||||||
|
|
||||||
await LoginAsync(_creds.Token);
|
await LoginAsync(_creds.Token);
|
||||||
|
|
||||||
@@ -274,8 +273,7 @@ public sealed class Bot : IBot
|
|||||||
Helpers.ReadErrorAndExit(9);
|
Helpers.ReadErrorAndExit(9);
|
||||||
}
|
}
|
||||||
|
|
||||||
sw.Stop();
|
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s", Client.ShardId, Stopwatch.GetElapsedTime(startTime).TotalSeconds);
|
||||||
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s", Client.ShardId, sw.Elapsed.TotalSeconds);
|
|
||||||
var commandHandler = Services.GetRequiredService<CommandHandler>();
|
var commandHandler = Services.GetRequiredService<CommandHandler>();
|
||||||
|
|
||||||
// start handling messages received in commandhandler
|
// start handling messages received in commandhandler
|
||||||
|
11
src/NadekoBot/Db/Models/HoneypotChannel.cs
Normal file
11
src/NadekoBot/Db/Models/HoneypotChannel.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace NadekoBot.Db.Models;
|
||||||
|
|
||||||
|
public class HoneypotChannel
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public ulong GuildId { get; set; }
|
||||||
|
|
||||||
|
public ulong ChannelId { get; set; }
|
||||||
|
}
|
@@ -1,30 +1,6 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
namespace NadekoBot.Db.Models;
|
namespace NadekoBot.Db.Models;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contains data about usage of Patron-Only commands per user
|
|
||||||
/// in order to provide support for quota limitations
|
|
||||||
/// (allow user x who is pledging amount y to use the specified command only
|
|
||||||
/// x amount of times in the specified time period)
|
|
||||||
/// </summary>
|
|
||||||
public class PatronQuota
|
|
||||||
{
|
|
||||||
public ulong UserId { get; set; }
|
|
||||||
public FeatureType FeatureType { get; set; }
|
|
||||||
public string Feature { get; set; }
|
|
||||||
public uint HourlyCount { get; set; }
|
|
||||||
public uint DailyCount { get; set; }
|
|
||||||
public uint MonthlyCount { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum FeatureType
|
|
||||||
{
|
|
||||||
Command,
|
|
||||||
Group,
|
|
||||||
Module,
|
|
||||||
Limit
|
|
||||||
}
|
|
||||||
|
|
||||||
public class PatronUser
|
public class PatronUser
|
||||||
{
|
{
|
||||||
public string UniquePlatformUserId { get; set; }
|
public string UniquePlatformUserId { get; set; }
|
||||||
|
@@ -53,14 +53,13 @@ public abstract class NadekoContext : DbContext
|
|||||||
|
|
||||||
public DbSet<PatronUser> Patrons { get; set; }
|
public DbSet<PatronUser> Patrons { get; set; }
|
||||||
|
|
||||||
public DbSet<PatronQuota> PatronQuotas { get; set; }
|
|
||||||
|
|
||||||
public DbSet<StreamOnlineMessage> StreamOnlineMessages { get; set; }
|
public DbSet<StreamOnlineMessage> StreamOnlineMessages { get; set; }
|
||||||
|
|
||||||
public DbSet<StickyRole> StickyRoles { get; set; }
|
public DbSet<StickyRole> StickyRoles { get; set; }
|
||||||
|
|
||||||
public DbSet<TodoModel> Todos { get; set; }
|
public DbSet<TodoModel> Todos { get; set; }
|
||||||
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
|
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
|
||||||
|
public DbSet<HoneypotChannel> HoneyPotChannels { get; set; }
|
||||||
|
|
||||||
// todo add guild colors
|
// todo add guild colors
|
||||||
// public DbSet<GuildColors> GuildColors { get; set; }
|
// public DbSet<GuildColors> GuildColors { get; set; }
|
||||||
@@ -597,16 +596,6 @@ public abstract class NadekoContext : DbContext
|
|||||||
});
|
});
|
||||||
|
|
||||||
// quotes are per user id
|
// quotes are per user id
|
||||||
modelBuilder.Entity<PatronQuota>(pq =>
|
|
||||||
{
|
|
||||||
pq.HasIndex(x => x.UserId).IsUnique(false);
|
|
||||||
pq.HasKey(x => new
|
|
||||||
{
|
|
||||||
x.UserId,
|
|
||||||
x.FeatureType,
|
|
||||||
x.Feature
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
3784
src/NadekoBot/Migrations/Mysql/20240611180516_remove-patron-limits.Designer.cs
generated
Normal file
3784
src/NadekoBot/Migrations/Mysql/20240611180516_remove-patron-limits.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations.Mysql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class removepatronlimits : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "patronquotas");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "patronquotas",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
userid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
|
||||||
|
featuretype = table.Column<int>(type: "int", nullable: false),
|
||||||
|
feature = table.Column<string>(type: "varchar(255)", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
dailycount = table.Column<uint>(type: "int unsigned", nullable: false),
|
||||||
|
hourlycount = table.Column<uint>(type: "int unsigned", nullable: false),
|
||||||
|
monthlycount = table.Column<uint>(type: "int unsigned", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_patronquotas", x => new { x.userid, x.featuretype, x.feature });
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_patronquotas_userid",
|
||||||
|
table: "patronquotas",
|
||||||
|
column: "userid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3803
src/NadekoBot/Migrations/Mysql/20240627033532_honeypot.Designer.cs
generated
Normal file
3803
src/NadekoBot/Migrations/Mysql/20240627033532_honeypot.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
src/NadekoBot/Migrations/Mysql/20240627033532_honeypot.cs
Normal file
36
src/NadekoBot/Migrations/Mysql/20240627033532_honeypot.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations.Mysql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class honeypot : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "honeypotchannels",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
guildid = table.Column<ulong>(type: "bigint unsigned", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
channelid = table.Column<ulong>(type: "bigint unsigned", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_honeypotchannels", x => x.guildid);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "honeypotchannels");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1388,6 +1388,25 @@ namespace NadekoBot.Migrations.Mysql
|
|||||||
b.ToTable("guildconfigs", (string)null);
|
b.ToTable("guildconfigs", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NadekoBot.Db.Models.HoneypotChannel", b =>
|
||||||
|
{
|
||||||
|
b.Property<ulong>("GuildId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint unsigned")
|
||||||
|
.HasColumnName("guildid");
|
||||||
|
|
||||||
|
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<ulong>("GuildId"));
|
||||||
|
|
||||||
|
b.Property<ulong>("ChannelId")
|
||||||
|
.HasColumnType("bigint unsigned")
|
||||||
|
.HasColumnName("channelid");
|
||||||
|
|
||||||
|
b.HasKey("GuildId")
|
||||||
|
.HasName("pk_honeypotchannels");
|
||||||
|
|
||||||
|
b.ToTable("honeypotchannels", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.IgnoredLogItem", b =>
|
modelBuilder.Entity("NadekoBot.Db.Models.IgnoredLogItem", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -1718,41 +1737,6 @@ namespace NadekoBot.Migrations.Mysql
|
|||||||
b.ToTable("expressions", (string)null);
|
b.ToTable("expressions", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b =>
|
|
||||||
{
|
|
||||||
b.Property<ulong>("UserId")
|
|
||||||
.HasColumnType("bigint unsigned")
|
|
||||||
.HasColumnName("userid");
|
|
||||||
|
|
||||||
b.Property<int>("FeatureType")
|
|
||||||
.HasColumnType("int")
|
|
||||||
.HasColumnName("featuretype");
|
|
||||||
|
|
||||||
b.Property<string>("Feature")
|
|
||||||
.HasColumnType("varchar(255)")
|
|
||||||
.HasColumnName("feature");
|
|
||||||
|
|
||||||
b.Property<uint>("DailyCount")
|
|
||||||
.HasColumnType("int unsigned")
|
|
||||||
.HasColumnName("dailycount");
|
|
||||||
|
|
||||||
b.Property<uint>("HourlyCount")
|
|
||||||
.HasColumnType("int unsigned")
|
|
||||||
.HasColumnName("hourlycount");
|
|
||||||
|
|
||||||
b.Property<uint>("MonthlyCount")
|
|
||||||
.HasColumnType("int unsigned")
|
|
||||||
.HasColumnName("monthlycount");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "FeatureType", "Feature")
|
|
||||||
.HasName("pk_patronquotas");
|
|
||||||
|
|
||||||
b.HasIndex("UserId")
|
|
||||||
.HasDatabaseName("ix_patronquotas_userid");
|
|
||||||
|
|
||||||
b.ToTable("patronquotas", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
|
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
|
||||||
{
|
{
|
||||||
b.Property<ulong>("UserId")
|
b.Property<ulong>("UserId")
|
||||||
|
3781
src/NadekoBot/Migrations/PostgreSql/20240611180506_remove-patron-limits.Designer.cs
generated
Normal file
3781
src/NadekoBot/Migrations/PostgreSql/20240611180506_remove-patron-limits.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations.PostgreSql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class removepatronlimits : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "patronquotas");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "patronquotas",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
featuretype = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
feature = table.Column<string>(type: "text", nullable: false),
|
||||||
|
dailycount = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
hourlycount = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
monthlycount = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_patronquotas", x => new { x.userid, x.featuretype, x.feature });
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "ix_patronquotas_userid",
|
||||||
|
table: "patronquotas",
|
||||||
|
column: "userid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3798
src/NadekoBot/Migrations/PostgreSql/20240627033522_honeypot.Designer.cs
generated
Normal file
3798
src/NadekoBot/Migrations/PostgreSql/20240627033522_honeypot.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations.PostgreSql
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class honeypot : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "honeypotchannels",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
|
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk_honeypotchannels", x => x.guildid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "honeypotchannels");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1387,6 +1387,23 @@ namespace NadekoBot.Migrations.PostgreSql
|
|||||||
b.ToTable("guildconfigs", (string)null);
|
b.ToTable("guildconfigs", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NadekoBot.Db.Models.HoneypotChannel", b =>
|
||||||
|
{
|
||||||
|
b.Property<decimal>("GuildId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("numeric(20,0)")
|
||||||
|
.HasColumnName("guildid");
|
||||||
|
|
||||||
|
b.Property<decimal>("ChannelId")
|
||||||
|
.HasColumnType("numeric(20,0)")
|
||||||
|
.HasColumnName("channelid");
|
||||||
|
|
||||||
|
b.HasKey("GuildId")
|
||||||
|
.HasName("pk_honeypotchannels");
|
||||||
|
|
||||||
|
b.ToTable("honeypotchannels", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.IgnoredLogItem", b =>
|
modelBuilder.Entity("NadekoBot.Db.Models.IgnoredLogItem", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -1717,41 +1734,6 @@ namespace NadekoBot.Migrations.PostgreSql
|
|||||||
b.ToTable("expressions", (string)null);
|
b.ToTable("expressions", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b =>
|
|
||||||
{
|
|
||||||
b.Property<decimal>("UserId")
|
|
||||||
.HasColumnType("numeric(20,0)")
|
|
||||||
.HasColumnName("userid");
|
|
||||||
|
|
||||||
b.Property<int>("FeatureType")
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasColumnName("featuretype");
|
|
||||||
|
|
||||||
b.Property<string>("Feature")
|
|
||||||
.HasColumnType("text")
|
|
||||||
.HasColumnName("feature");
|
|
||||||
|
|
||||||
b.Property<long>("DailyCount")
|
|
||||||
.HasColumnType("bigint")
|
|
||||||
.HasColumnName("dailycount");
|
|
||||||
|
|
||||||
b.Property<long>("HourlyCount")
|
|
||||||
.HasColumnType("bigint")
|
|
||||||
.HasColumnName("hourlycount");
|
|
||||||
|
|
||||||
b.Property<long>("MonthlyCount")
|
|
||||||
.HasColumnType("bigint")
|
|
||||||
.HasColumnName("monthlycount");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "FeatureType", "Feature")
|
|
||||||
.HasName("pk_patronquotas");
|
|
||||||
|
|
||||||
b.HasIndex("UserId")
|
|
||||||
.HasDatabaseName("ix_patronquotas_userid");
|
|
||||||
|
|
||||||
b.ToTable("patronquotas", (string)null);
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
|
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
|
||||||
{
|
{
|
||||||
b.Property<decimal>("UserId")
|
b.Property<decimal>("UserId")
|
||||||
|
@@ -11,6 +11,8 @@ namespace NadekoBot.Migrations
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
MigrationQueries.GuildConfigCleanup(migrationBuilder);
|
||||||
|
|
||||||
migrationBuilder.DropForeignKey(
|
migrationBuilder.DropForeignKey(
|
||||||
name: "FK_AntiRaidSetting_GuildConfigs_GuildConfigId",
|
name: "FK_AntiRaidSetting_GuildConfigs_GuildConfigId",
|
||||||
table: "AntiRaidSetting");
|
table: "AntiRaidSetting");
|
||||||
|
2921
src/NadekoBot/Migrations/Sqlite/20240611180456_remove-patron-limits.Designer.cs
generated
Normal file
2921
src/NadekoBot/Migrations/Sqlite/20240611180456_remove-patron-limits.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class removepatronlimits : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PatronQuotas");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PatronQuotas",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
|
FeatureType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Feature = table.Column<string>(type: "TEXT", nullable: false),
|
||||||
|
DailyCount = table.Column<uint>(type: "INTEGER", nullable: false),
|
||||||
|
HourlyCount = table.Column<uint>(type: "INTEGER", nullable: false),
|
||||||
|
MonthlyCount = table.Column<uint>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PatronQuotas", x => new { x.UserId, x.FeatureType, x.Feature });
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PatronQuotas_UserId",
|
||||||
|
table: "PatronQuotas",
|
||||||
|
column: "UserId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2935
src/NadekoBot/Migrations/Sqlite/20240627033508_honeypot.Designer.cs
generated
Normal file
2935
src/NadekoBot/Migrations/Sqlite/20240627033508_honeypot.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
src/NadekoBot/Migrations/Sqlite/20240627033508_honeypot.cs
Normal file
34
src/NadekoBot/Migrations/Sqlite/20240627033508_honeypot.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NadekoBot.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class honeypot : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "HoneyPotChannels",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_HoneyPotChannels", x => x.GuildId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "HoneyPotChannels");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1033,6 +1033,20 @@ namespace NadekoBot.Migrations
|
|||||||
b.ToTable("GuildConfigs");
|
b.ToTable("GuildConfigs");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NadekoBot.Db.Models.HoneypotChannel", b =>
|
||||||
|
{
|
||||||
|
b.Property<ulong>("GuildId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<ulong>("ChannelId")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("GuildId");
|
||||||
|
|
||||||
|
b.ToTable("HoneyPotChannels");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.IgnoredLogItem", b =>
|
modelBuilder.Entity("NadekoBot.Db.Models.IgnoredLogItem", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -1279,33 +1293,6 @@ namespace NadekoBot.Migrations
|
|||||||
b.ToTable("Expressions");
|
b.ToTable("Expressions");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b =>
|
|
||||||
{
|
|
||||||
b.Property<ulong>("UserId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("FeatureType")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("Feature")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<uint>("DailyCount")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<uint>("HourlyCount")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<uint>("MonthlyCount")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("UserId", "FeatureType", "Feature");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("PatronQuotas");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
|
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
|
||||||
{
|
{
|
||||||
b.Property<ulong>("UserId")
|
b.Property<ulong>("UserId")
|
||||||
|
@@ -61,17 +61,66 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, INService
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// delete guild configs
|
||||||
await ctx.GetTable<GuildConfig>()
|
await ctx.GetTable<GuildConfig>()
|
||||||
.Where(x => !tempTable.Select(x => x.GuildId)
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
.Contains(x.GuildId))
|
.Contains(x.GuildId))
|
||||||
.DeleteAsync();
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete guild xp
|
||||||
await ctx.GetTable<UserXpStats>()
|
await ctx.GetTable<UserXpStats>()
|
||||||
.Where(x => !tempTable.Select(x => x.GuildId)
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
.Contains(x.GuildId))
|
.Contains(x.GuildId))
|
||||||
.DeleteAsync();
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete expressions
|
||||||
|
await ctx.GetTable<NadekoExpression>()
|
||||||
|
.Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId.Value))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete quotes
|
||||||
|
await ctx.GetTable<Quote>()
|
||||||
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete planted currencies
|
||||||
|
await ctx.GetTable<PlantedCurrency>()
|
||||||
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete image only channels
|
||||||
|
await ctx.GetTable<ImageOnlyChannel>()
|
||||||
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete reaction roles
|
||||||
|
await ctx.GetTable<ReactionRoleV2>()
|
||||||
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete ignored users
|
||||||
|
await ctx.GetTable<DiscordPermOverride>()
|
||||||
|
.Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId.Value))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete perm overrides
|
||||||
|
await ctx.GetTable<DiscordPermOverride>()
|
||||||
|
.Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId.Value))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
|
// delete repeaters
|
||||||
|
await ctx.GetTable<Repeater>()
|
||||||
|
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||||
|
.Contains(x.GuildId))
|
||||||
|
.DeleteAsync();
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
GuildCount = guildIds.Keys.Count,
|
GuildCount = guildIds.Keys.Count,
|
||||||
|
@@ -225,5 +225,19 @@ public partial class Administration
|
|||||||
if (!enabled)
|
if (!enabled)
|
||||||
await Response().Pending(strs.greetdmmsg_enable($"`{prefix}greetdm`")).SendAsync();
|
await Response().Pending(strs.greetdmmsg_enable($"`{prefix}greetdm`")).SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[UserPerm(GuildPerm.ManageGuild)]
|
||||||
|
[Ratelimit(5)]
|
||||||
|
public async Task BoostTest([Leftover] IGuildUser? user = null)
|
||||||
|
{
|
||||||
|
user ??= (IGuildUser)ctx.User;
|
||||||
|
|
||||||
|
await _service.BoostTest((ITextChannel)ctx.Channel, user);
|
||||||
|
var enabled = _service.GetBoostEnabled(ctx.Guild.Id);
|
||||||
|
if (!enabled)
|
||||||
|
await Response().Pending(strs.boostmsg_enable($"`{prefix}boost`")).SendAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -630,6 +630,13 @@ public class GreetService : INService, IReadyExecutor
|
|||||||
return conf.SendChannelByeMessage;
|
return conf.SendChannelByeMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool GetBoostEnabled(ulong guildId)
|
||||||
|
{
|
||||||
|
using var uow = _db.GetDbContext();
|
||||||
|
var conf = uow.GuildConfigsForId(guildId, set => set);
|
||||||
|
return conf.SendBoostMessage;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Test Messages
|
#region Test Messages
|
||||||
|
@@ -0,0 +1,95 @@
|
|||||||
|
using LinqToDB;
|
||||||
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
|
using NadekoBot.Db.Models;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Administration.Honeypot;
|
||||||
|
|
||||||
|
public sealed class HoneyPotService : IHoneyPotService, IReadyExecutor, IExecNoCommand, INService
|
||||||
|
{
|
||||||
|
private readonly DbService _db;
|
||||||
|
private readonly CommandHandler _handler;
|
||||||
|
|
||||||
|
private ConcurrentHashSet<ulong> _channels = new();
|
||||||
|
|
||||||
|
private Channel<SocketGuildUser> _punishments = Channel.CreateBounded<SocketGuildUser>(
|
||||||
|
new BoundedChannelOptions(100)
|
||||||
|
{
|
||||||
|
FullMode = BoundedChannelFullMode.DropOldest,
|
||||||
|
SingleReader = true,
|
||||||
|
SingleWriter = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
public HoneyPotService(DbService db, CommandHandler handler)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ToggleHoneypotChannel(ulong guildId, ulong channelId)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
|
var deleted = await uow.HoneyPotChannels
|
||||||
|
.Where(x => x.GuildId == guildId)
|
||||||
|
.DeleteWithOutputAsync();
|
||||||
|
|
||||||
|
if (deleted.Length > 0)
|
||||||
|
{
|
||||||
|
_channels.TryRemove(deleted[0].ChannelId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await uow.HoneyPotChannels
|
||||||
|
.ToLinqToDBTable()
|
||||||
|
.InsertAsync(() => new HoneypotChannel
|
||||||
|
{
|
||||||
|
GuildId = guildId,
|
||||||
|
ChannelId = channelId
|
||||||
|
});
|
||||||
|
|
||||||
|
_channels.Add(channelId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnReadyAsync()
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
|
var channels = await uow.HoneyPotChannels
|
||||||
|
.Select(x => x.ChannelId)
|
||||||
|
.ToListAsyncLinqToDB();
|
||||||
|
|
||||||
|
_channels = new(channels);
|
||||||
|
|
||||||
|
while (await _punishments.Reader.WaitToReadAsync())
|
||||||
|
{
|
||||||
|
while (_punishments.Reader.TryRead(out var user))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.Information("Honeypot caught user {User} [{UserId}]", user, user.Id);
|
||||||
|
await user.BanAsync(pruneDays: 1);
|
||||||
|
await user.Guild.RemoveBanAsync(user.Id);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Warning(e, "Failed banning {User} due to {Error}", user, e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
|
||||||
|
{
|
||||||
|
if (_channels.Contains(msg.Channel.Id) && msg.Author is SocketGuildUser sgu)
|
||||||
|
{
|
||||||
|
if (!sgu.GuildPermissions.BanMembers)
|
||||||
|
await _punishments.Writer.WriteAsync(sgu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
using NadekoBot.Modules.Administration.Honeypot;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Administration;
|
||||||
|
|
||||||
|
public partial class Administration
|
||||||
|
{
|
||||||
|
[Group]
|
||||||
|
public partial class HoneypotCommands : NadekoModule
|
||||||
|
{
|
||||||
|
private readonly IHoneyPotService _service;
|
||||||
|
|
||||||
|
public HoneypotCommands(IHoneyPotService service)
|
||||||
|
=> _service = service;
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
[RequireUserPermission(GuildPermission.Administrator)]
|
||||||
|
[RequireBotPermission(GuildPermission.BanMembers)]
|
||||||
|
public async Task Honeypot()
|
||||||
|
{
|
||||||
|
var enabled = await _service.ToggleHoneypotChannel(ctx.Guild.Id, ctx.Channel.Id);
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
await Response().Confirm(strs.honeypot_on).SendAsync();
|
||||||
|
else
|
||||||
|
await Response().Confirm(strs.honeypot_off).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
namespace NadekoBot.Modules.Administration.Honeypot;
|
||||||
|
|
||||||
|
public interface IHoneyPotService
|
||||||
|
{
|
||||||
|
public Task<bool> ToggleHoneypotChannel(ulong guildId, ulong channelId);
|
||||||
|
}
|
@@ -45,23 +45,43 @@ public partial class Administration
|
|||||||
var progressMsg = await Response().Pending(strs.prune_progress(0, 100)).SendAsync();
|
var progressMsg = await Response().Pending(strs.prune_progress(0, 100)).SendAsync();
|
||||||
var progress = GetProgressTracker(progressMsg);
|
var progress = GetProgressTracker(progressMsg);
|
||||||
|
|
||||||
|
PruneResult result;
|
||||||
if (opts.Safe)
|
if (opts.Safe)
|
||||||
await _service.PruneWhere((ITextChannel)ctx.Channel,
|
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||||
100,
|
100,
|
||||||
x => x.Author.Id == user.Id && !x.IsPinned,
|
x => x.Author.Id == user.Id && !x.IsPinned,
|
||||||
progress,
|
progress,
|
||||||
opts.After);
|
opts.After);
|
||||||
else
|
else
|
||||||
await _service.PruneWhere((ITextChannel)ctx.Channel,
|
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||||
100,
|
100,
|
||||||
x => x.Author.Id == user.Id,
|
x => x.Author.Id == user.Id,
|
||||||
progress,
|
progress,
|
||||||
opts.After);
|
opts.After);
|
||||||
|
|
||||||
ctx.Message.DeleteAfter(3);
|
ctx.Message.DeleteAfter(3);
|
||||||
|
|
||||||
|
await SendResult(result);
|
||||||
await progressMsg.DeleteAsync();
|
await progressMsg.DeleteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SendResult(PruneResult result)
|
||||||
|
{
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case PruneResult.Success:
|
||||||
|
break;
|
||||||
|
case PruneResult.AlreadyRunning:
|
||||||
|
break;
|
||||||
|
case PruneResult.FeatureLimit:
|
||||||
|
await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(result), result, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// prune x
|
// prune x
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
@@ -83,19 +103,21 @@ public partial class Administration
|
|||||||
var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync();
|
var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync();
|
||||||
var progress = GetProgressTracker(progressMsg);
|
var progress = GetProgressTracker(progressMsg);
|
||||||
|
|
||||||
|
PruneResult result;
|
||||||
if (opts.Safe)
|
if (opts.Safe)
|
||||||
await _service.PruneWhere((ITextChannel)ctx.Channel,
|
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||||
count,
|
count,
|
||||||
x => !x.IsPinned && x.Id != progressMsg.Id,
|
x => !x.IsPinned && x.Id != progressMsg.Id,
|
||||||
progress,
|
progress,
|
||||||
opts.After);
|
opts.After);
|
||||||
else
|
else
|
||||||
await _service.PruneWhere((ITextChannel)ctx.Channel,
|
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||||
count,
|
count,
|
||||||
x => x.Id != progressMsg.Id,
|
x => x.Id != progressMsg.Id,
|
||||||
progress,
|
progress,
|
||||||
opts.After);
|
opts.After);
|
||||||
|
|
||||||
|
await SendResult(result);
|
||||||
await progressMsg.DeleteAsync();
|
await progressMsg.DeleteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,9 +177,10 @@ public partial class Administration
|
|||||||
var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync();
|
var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync();
|
||||||
var progress = GetProgressTracker(progressMsg);
|
var progress = GetProgressTracker(progressMsg);
|
||||||
|
|
||||||
|
PruneResult result;
|
||||||
if (opts.Safe)
|
if (opts.Safe)
|
||||||
{
|
{
|
||||||
await _service.PruneWhere((ITextChannel)ctx.Channel,
|
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||||
count,
|
count,
|
||||||
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned,
|
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned,
|
||||||
progress,
|
progress,
|
||||||
@@ -166,7 +189,7 @@ public partial class Administration
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await _service.PruneWhere((ITextChannel)ctx.Channel,
|
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||||
count,
|
count,
|
||||||
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks,
|
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks,
|
||||||
progress,
|
progress,
|
||||||
@@ -174,6 +197,7 @@ public partial class Administration
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await SendResult(result);
|
||||||
await progressMsg.DeleteAsync();
|
await progressMsg.DeleteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
#nullable disable
|
||||||
|
namespace NadekoBot.Modules.Administration.Services;
|
||||||
|
|
||||||
|
public enum PruneResult
|
||||||
|
{
|
||||||
|
Success,
|
||||||
|
AlreadyRunning,
|
||||||
|
FeatureLimit,
|
||||||
|
}
|
@@ -1,4 +1,6 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
using NadekoBot.Modules.Patronage;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Administration.Services;
|
namespace NadekoBot.Modules.Administration.Services;
|
||||||
|
|
||||||
public class PruneService : INService
|
public class PruneService : INService
|
||||||
@@ -7,11 +9,15 @@ public class PruneService : INService
|
|||||||
private readonly ConcurrentDictionary<ulong, CancellationTokenSource> _pruningGuilds = new();
|
private readonly ConcurrentDictionary<ulong, CancellationTokenSource> _pruningGuilds = new();
|
||||||
private readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14);
|
private readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14);
|
||||||
private readonly ILogCommandService _logService;
|
private readonly ILogCommandService _logService;
|
||||||
|
private readonly IPatronageService _ps;
|
||||||
|
|
||||||
public PruneService(ILogCommandService logService)
|
public PruneService(ILogCommandService logService, IPatronageService ps)
|
||||||
=> _logService = logService;
|
{
|
||||||
|
_logService = logService;
|
||||||
|
_ps = ps;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task PruneWhere(
|
public async Task<PruneResult> PruneWhere(
|
||||||
ITextChannel channel,
|
ITextChannel channel,
|
||||||
int amount,
|
int amount,
|
||||||
Func<IMessage, bool> predicate,
|
Func<IMessage, bool> predicate,
|
||||||
@@ -26,7 +32,12 @@ public class PruneService : INService
|
|||||||
|
|
||||||
using var cancelSource = new CancellationTokenSource();
|
using var cancelSource = new CancellationTokenSource();
|
||||||
if (!_pruningGuilds.TryAdd(channel.GuildId, cancelSource))
|
if (!_pruningGuilds.TryAdd(channel.GuildId, cancelSource))
|
||||||
return;
|
return PruneResult.AlreadyRunning;
|
||||||
|
|
||||||
|
if (!await _ps.LimitHitAsync(LimitedFeatureName.Prune, channel.Guild.OwnerId))
|
||||||
|
{
|
||||||
|
return PruneResult.FeatureLimit;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -47,7 +58,7 @@ public class PruneService : INService
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
if (!msgs.Any())
|
if (!msgs.Any())
|
||||||
return;
|
return PruneResult.Success;
|
||||||
|
|
||||||
lastMessage = msgs[^1];
|
lastMessage = msgs[^1];
|
||||||
|
|
||||||
@@ -88,6 +99,8 @@ public class PruneService : INService
|
|||||||
{
|
{
|
||||||
_pruningGuilds.TryRemove(channel.GuildId, out _);
|
_pruningGuilds.TryRemove(channel.GuildId, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return PruneResult.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CancelAsync(ulong guildId)
|
public async Task<bool> CancelAsync(ulong guildId)
|
||||||
|
@@ -18,7 +18,7 @@ public interface IReactionRoleService
|
|||||||
/// <param name="group"></param>
|
/// <param name="group"></param>
|
||||||
/// <param name="levelReq"></param>
|
/// <param name="levelReq"></param>
|
||||||
/// <returns>The result of the operation</returns>
|
/// <returns>The result of the operation</returns>
|
||||||
Task<OneOf<Success, FeatureLimit>> AddReactionRole(
|
Task<OneOf<Success, Error>> AddReactionRole(
|
||||||
IGuild guild,
|
IGuild guild,
|
||||||
IMessage msg,
|
IMessage msg,
|
||||||
string emote,
|
string emote,
|
||||||
|
@@ -55,12 +55,10 @@ public partial class Administration
|
|||||||
|
|
||||||
await res.Match(
|
await res.Match(
|
||||||
_ => ctx.OkAsync(),
|
_ => ctx.OkAsync(),
|
||||||
fl =>
|
async fl =>
|
||||||
{
|
{
|
||||||
_ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
|
_ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
|
||||||
return !fl.IsPatronLimit
|
await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
|
||||||
? Response().Error(strs.limit_reached(fl.Quota)).SendAsync()
|
|
||||||
: Response().Pending(strs.feature_limit_reached_owner(fl.Quota, fl.Name)).SendAsync();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,22 +21,16 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
|
|||||||
private readonly SemaphoreSlim _assignementLock = new(1, 1);
|
private readonly SemaphoreSlim _assignementLock = new(1, 1);
|
||||||
private readonly IPatronageService _ps;
|
private readonly IPatronageService _ps;
|
||||||
|
|
||||||
private static readonly FeatureLimitKey _reroFLKey = new()
|
|
||||||
{
|
|
||||||
Key = "rero:max_count",
|
|
||||||
PrettyName = "Reaction Role"
|
|
||||||
};
|
|
||||||
|
|
||||||
public ReactionRolesService(
|
public ReactionRolesService(
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
|
IPatronageService ps,
|
||||||
DbService db,
|
DbService db,
|
||||||
IBotCredentials creds,
|
IBotCredentials creds)
|
||||||
IPatronageService ps)
|
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_ps = ps;
|
|
||||||
_client = client;
|
_client = client;
|
||||||
_creds = creds;
|
_creds = creds;
|
||||||
|
_ps = ps;
|
||||||
_cache = new();
|
_cache = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +236,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
|
|||||||
/// <param name="group"></param>
|
/// <param name="group"></param>
|
||||||
/// <param name="levelReq"></param>
|
/// <param name="levelReq"></param>
|
||||||
/// <returns>The result of the operation</returns>
|
/// <returns>The result of the operation</returns>
|
||||||
public async Task<OneOf<Success, FeatureLimit>> AddReactionRole(
|
public async Task<OneOf<Success, Error>> AddReactionRole(
|
||||||
IGuild guild,
|
IGuild guild,
|
||||||
IMessage msg,
|
IMessage msg,
|
||||||
string emote,
|
string emote,
|
||||||
@@ -261,9 +255,12 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
|
|||||||
.Where(x => x.GuildId == guild.Id)
|
.Where(x => x.GuildId == guild.Id)
|
||||||
.CountAsync();
|
.CountAsync();
|
||||||
|
|
||||||
var result = await _ps.TryGetFeatureLimitAsync(_reroFLKey, guild.OwnerId, 50);
|
var limit = await _ps.GetUserLimit(LimitedFeatureName.ReactionRole, guild.OwnerId);
|
||||||
if (result.Quota != -1 && activeReactionRoles >= result.Quota)
|
|
||||||
return result;
|
if (!_creds.IsOwner(guild.OwnerId) && (activeReactionRoles >= limit.Quota && limit.Quota >= 0))
|
||||||
|
{
|
||||||
|
return new Error();
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.GetTable<ReactionRoleV2>()
|
await ctx.GetTable<ReactionRoleV2>()
|
||||||
.InsertOrUpdateAsync(() => new()
|
.InsertOrUpdateAsync(() => new()
|
||||||
|
@@ -0,0 +1,14 @@
|
|||||||
|
// namespace NadekoBot.Modules.Administration;
|
||||||
|
//
|
||||||
|
// public partial class Administration
|
||||||
|
// {
|
||||||
|
// [Group]
|
||||||
|
// public partial class TicketCommands : NadekoModule
|
||||||
|
// {
|
||||||
|
// [Cmd]
|
||||||
|
// public async Task Ticket()
|
||||||
|
// {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
@@ -74,6 +74,27 @@ public partial class Gambling
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[OwnerOnly]
|
||||||
|
public async Task BankBalance([Leftover] IUser user)
|
||||||
|
{
|
||||||
|
var bal = await _bank.GetBalanceAsync(user.Id);
|
||||||
|
|
||||||
|
var eb = _sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithDescription(GetText(strs.bank_balance_other(user.ToString(), N(bal))));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Response().User(ctx.User).Embed(eb).SendAsync();
|
||||||
|
await ctx.OkAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await Response().Error(strs.cant_dm).SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task BankTakeInternalAsync(long amount, ulong userId)
|
private async Task BankTakeInternalAsync(long amount, ulong userId)
|
||||||
{
|
{
|
||||||
if (await _bank.TakeAsync(userId, amount))
|
if (await _bank.TakeAsync(userId, amount))
|
||||||
|
@@ -3,6 +3,7 @@ using NadekoBot.Common.TypeReaders;
|
|||||||
using NadekoBot.Modules.Gambling.Common;
|
using NadekoBot.Modules.Gambling.Common;
|
||||||
using NadekoBot.Modules.Gambling.Common.Blackjack;
|
using NadekoBot.Modules.Gambling.Common.Blackjack;
|
||||||
using NadekoBot.Modules.Gambling.Services;
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
|
using NadekoBot.Modules.Utility;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Gambling;
|
namespace NadekoBot.Modules.Gambling;
|
||||||
|
|
||||||
|
@@ -17,7 +17,8 @@ public partial class Gambling
|
|||||||
private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new();
|
private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new();
|
||||||
private readonly IImageCache _images;
|
private readonly IImageCache _images;
|
||||||
|
|
||||||
public DrawCommands(IImageCache images, GamblingConfigService gcs) : base(gcs)
|
public DrawCommands(IImageCache images, GamblingConfigService gcs)
|
||||||
|
: base(gcs)
|
||||||
=> _images = images;
|
=> _images = images;
|
||||||
|
|
||||||
private async Task InternalDraw(int count, ulong? guildId = null)
|
private async Task InternalDraw(int count, ulong? guildId = null)
|
||||||
@@ -136,18 +137,28 @@ public partial class Gambling
|
|||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
public Task BetDraw([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, InputValueGuess val, InputColorGuess? col = null)
|
public Task BetDraw(
|
||||||
|
[OverrideTypeReader(typeof(BalanceTypeReader))]
|
||||||
|
long amount,
|
||||||
|
InputValueGuess val,
|
||||||
|
InputColorGuess? col = null)
|
||||||
=> BetDrawInternal(amount, val, col);
|
=> BetDrawInternal(amount, val, col);
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
public Task BetDraw([OverrideTypeReader(typeof(BalanceTypeReader))] long amount, InputColorGuess col, InputValueGuess? val = null)
|
public Task BetDraw(
|
||||||
|
[OverrideTypeReader(typeof(BalanceTypeReader))]
|
||||||
|
long amount,
|
||||||
|
InputColorGuess col,
|
||||||
|
InputValueGuess? val = null)
|
||||||
=> BetDrawInternal(amount, val, col);
|
=> BetDrawInternal(amount, val, col);
|
||||||
|
|
||||||
public async Task BetDrawInternal(long amount, InputValueGuess? val, InputColorGuess? col)
|
public async Task BetDrawInternal(long amount, InputValueGuess? val, InputColorGuess? col)
|
||||||
{
|
{
|
||||||
if (amount <= 0)
|
if (!await CheckBetMandatory(amount))
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var res = await _service.BetDrawAsync(ctx.User.Id,
|
var res = await _service.BetDrawAsync(ctx.User.Id,
|
||||||
amount,
|
amount,
|
||||||
@@ -192,6 +203,7 @@ public partial class Gambling
|
|||||||
|
|
||||||
return $"{val} / {col}";
|
return $"{val} / {col}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetCardInfo(RegularCard card)
|
private string GetCardInfo(RegularCard card)
|
||||||
{
|
{
|
||||||
var val = (int)card.Value switch
|
var val = (int)card.Value switch
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.EntityFrameworkCore;
|
using LinqToDB.EntityFrameworkCore;
|
||||||
using NadekoBot.Db;
|
|
||||||
using NadekoBot.Db.Models;
|
using NadekoBot.Db.Models;
|
||||||
using NadekoBot.Modules.Gambling.Bank;
|
using NadekoBot.Modules.Gambling.Bank;
|
||||||
using NadekoBot.Modules.Gambling.Common;
|
using NadekoBot.Modules.Gambling.Common;
|
||||||
@@ -14,6 +13,7 @@ using System.Text;
|
|||||||
using NadekoBot.Modules.Gambling.Rps;
|
using NadekoBot.Modules.Gambling.Rps;
|
||||||
using NadekoBot.Common.TypeReaders;
|
using NadekoBot.Common.TypeReaders;
|
||||||
using NadekoBot.Modules.Patronage;
|
using NadekoBot.Modules.Patronage;
|
||||||
|
using NadekoBot.Modules.Utility;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Gambling;
|
namespace NadekoBot.Modules.Gambling;
|
||||||
|
|
||||||
@@ -27,9 +27,9 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
private readonly DownloadTracker _tracker;
|
private readonly DownloadTracker _tracker;
|
||||||
private readonly GamblingConfigService _configService;
|
private readonly GamblingConfigService _configService;
|
||||||
private readonly IBankService _bank;
|
private readonly IBankService _bank;
|
||||||
private readonly IPatronageService _ps;
|
|
||||||
private readonly IRemindService _remind;
|
private readonly IRemindService _remind;
|
||||||
private readonly GamblingTxTracker _gamblingTxTracker;
|
private readonly GamblingTxTracker _gamblingTxTracker;
|
||||||
|
private readonly IPatronageService _ps;
|
||||||
|
|
||||||
private IUserMessage rdMsg;
|
private IUserMessage rdMsg;
|
||||||
|
|
||||||
@@ -41,8 +41,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
DownloadTracker tracker,
|
DownloadTracker tracker,
|
||||||
GamblingConfigService configService,
|
GamblingConfigService configService,
|
||||||
IBankService bank,
|
IBankService bank,
|
||||||
IPatronageService ps,
|
|
||||||
IRemindService remind,
|
IRemindService remind,
|
||||||
|
IPatronageService patronage,
|
||||||
GamblingTxTracker gamblingTxTracker)
|
GamblingTxTracker gamblingTxTracker)
|
||||||
: base(configService)
|
: base(configService)
|
||||||
{
|
{
|
||||||
@@ -51,9 +51,9 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
_cs = currency;
|
_cs = currency;
|
||||||
_client = client;
|
_client = client;
|
||||||
_bank = bank;
|
_bank = bank;
|
||||||
_ps = ps;
|
|
||||||
_remind = remind;
|
_remind = remind;
|
||||||
_gamblingTxTracker = gamblingTxTracker;
|
_gamblingTxTracker = gamblingTxTracker;
|
||||||
|
_ps = patronage;
|
||||||
|
|
||||||
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
|
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
|
||||||
_enUsCulture.NumberDecimalDigits = 0;
|
_enUsCulture.NumberDecimalDigits = 0;
|
||||||
@@ -133,12 +133,6 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
await Response().Embed(embed).SendAsync();
|
await Response().Embed(embed).SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly FeatureLimitKey _timelyKey = new FeatureLimitKey()
|
|
||||||
{
|
|
||||||
Key = "timely:extra_percent",
|
|
||||||
PrettyName = "Timely"
|
|
||||||
};
|
|
||||||
|
|
||||||
private async Task RemindTimelyAction(SocketMessageComponent smc, DateTime when)
|
private async Task RemindTimelyAction(SocketMessageComponent smc, DateTime when)
|
||||||
{
|
{
|
||||||
var tt = TimestampTag.FromDateTime(when, TimestampTagStyles.Relative);
|
var tt = TimestampTag.FromDateTime(when, TimestampTagStyles.Relative);
|
||||||
@@ -154,6 +148,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
await smc.RespondConfirmAsync(_sender, GetText(strs.remind_timely(tt)), ephemeral: true);
|
await smc.RespondConfirmAsync(_sender, GetText(strs.remind_timely(tt)), ephemeral: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Creates timely reminder button, parameter in hours.
|
||||||
private NadekoInteractionBase CreateRemindMeInteraction(int period)
|
private NadekoInteractionBase CreateRemindMeInteraction(int period)
|
||||||
=> _inter
|
=> _inter
|
||||||
.Create(ctx.User.Id,
|
.Create(ctx.User.Id,
|
||||||
@@ -164,6 +159,17 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
(smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromHours(period)))
|
(smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromHours(period)))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Creates timely reminder button, parameter in milliseconds.
|
||||||
|
private NadekoInteractionBase CreateRemindMeInteraction(double ms)
|
||||||
|
=> _inter
|
||||||
|
.Create(ctx.User.Id,
|
||||||
|
new ButtonBuilder(
|
||||||
|
label: "Remind me",
|
||||||
|
emote: Emoji.Parse("⏰"),
|
||||||
|
customId: "timely:remind_me"),
|
||||||
|
(smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromMilliseconds(ms)))
|
||||||
|
);
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
public async Task Timely()
|
public async Task Timely()
|
||||||
{
|
{
|
||||||
@@ -175,25 +181,31 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var inter = CreateRemindMeInteraction(period);
|
if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } remainder)
|
||||||
|
|
||||||
if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } rem)
|
|
||||||
{
|
{
|
||||||
|
// Get correct time form remainder
|
||||||
|
var interaction = CreateRemindMeInteraction(remainder.TotalMilliseconds);
|
||||||
|
|
||||||
// Removes timely button if there is a timely reminder in DB
|
// Removes timely button if there is a timely reminder in DB
|
||||||
if (_service.UserHasTimelyReminder(ctx.User.Id))
|
if (_service.UserHasTimelyReminder(ctx.User.Id))
|
||||||
{
|
{
|
||||||
inter = null;
|
interaction = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var relativeTag = TimestampTag.FromDateTime(now.Add(rem), TimestampTagStyles.Relative);
|
var relativeTag = TimestampTag.FromDateTime(now.Add(remainder), TimestampTagStyles.Relative);
|
||||||
await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(inter).SendAsync();
|
await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(interaction).SendAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await _ps.TryGetFeatureLimitAsync(_timelyKey, ctx.User.Id, 0);
|
|
||||||
|
|
||||||
val = (int)(val * (1 + (result.Quota! * 0.01f)));
|
var patron = await _ps.GetPatronAsync(ctx.User.Id);
|
||||||
|
|
||||||
|
var percentBonus = (_ps.PercentBonus(patron) / 100f);
|
||||||
|
|
||||||
|
val += (int)(val * percentBonus);
|
||||||
|
|
||||||
|
var inter = CreateRemindMeInteraction(period);
|
||||||
|
|
||||||
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
|
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
|
||||||
|
|
||||||
@@ -892,6 +904,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||||||
private static readonly ImmutableArray<string> _emojis =
|
private static readonly ImmutableArray<string> _emojis =
|
||||||
new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray();
|
new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray();
|
||||||
|
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
public async Task LuckyLadder([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
|
public async Task LuckyLadder([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
|
||||||
{
|
{
|
||||||
|
@@ -247,7 +247,14 @@ public partial class Gambling
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var cmd = entry.Command.Replace("%you%", ctx.User.Id.ToString());
|
var buyer = (IGuildUser)ctx.User;
|
||||||
|
var cmd = entry.Command
|
||||||
|
.Replace("%you%", buyer.Mention)
|
||||||
|
.Replace("%you.mention%", buyer.Mention)
|
||||||
|
.Replace("%you.username%", buyer.Username)
|
||||||
|
.Replace("%you.name%", buyer.GlobalName ?? buyer.Username)
|
||||||
|
.Replace("%you.nick%", buyer.DisplayName);
|
||||||
|
|
||||||
var eb = _sender.CreateEmbed()
|
var eb = _sender.CreateEmbed()
|
||||||
.WithPendingColor()
|
.WithPendingColor()
|
||||||
.WithTitle("Executing shop command")
|
.WithTitle("Executing shop command")
|
||||||
@@ -259,6 +266,7 @@ public partial class Gambling
|
|||||||
GetProfitAmount(entry.Price),
|
GetProfitAmount(entry.Price),
|
||||||
new("shop", "sell", entry.Name));
|
new("shop", "sell", entry.Name));
|
||||||
|
|
||||||
|
await Task.Delay(250);
|
||||||
await _cmdHandler.TryRunCommand(guild,
|
await _cmdHandler.TryRunCommand(guild,
|
||||||
channel,
|
channel,
|
||||||
new DoAsUserMessage(
|
new DoAsUserMessage(
|
||||||
|
@@ -9,6 +9,7 @@ using SixLabors.ImageSharp.PixelFormats;
|
|||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
using NadekoBot.Modules.Gambling;
|
using NadekoBot.Modules.Gambling;
|
||||||
using NadekoBot.Common.TypeReaders;
|
using NadekoBot.Common.TypeReaders;
|
||||||
|
using NadekoBot.Modules.Utility;
|
||||||
using Color = SixLabors.ImageSharp.Color;
|
using Color = SixLabors.ImageSharp.Color;
|
||||||
using Image = SixLabors.ImageSharp.Image;
|
using Image = SixLabors.ImageSharp.Image;
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ using NadekoBot.Modules.Gambling.Common;
|
|||||||
using NadekoBot.Modules.Gambling.Common.Waifu;
|
using NadekoBot.Modules.Gambling.Common.Waifu;
|
||||||
using NadekoBot.Modules.Gambling.Services;
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
using NadekoBot.Db.Models;
|
using NadekoBot.Db.Models;
|
||||||
|
using TwitchLib.Api.Helix.Models.Teams;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Gambling;
|
namespace NadekoBot.Modules.Gambling;
|
||||||
|
|
||||||
@@ -317,7 +318,9 @@ public partial class Gambling
|
|||||||
.AddField(GetText(strs.price), N(wi.Price), true)
|
.AddField(GetText(strs.price), N(wi.Price), true)
|
||||||
.AddField(GetText(strs.claimed_by), wi.ClaimerName ?? nobody, true)
|
.AddField(GetText(strs.claimed_by), wi.ClaimerName ?? nobody, true)
|
||||||
.AddField(GetText(strs.likes), wi.AffinityName ?? nobody, true)
|
.AddField(GetText(strs.likes), wi.AffinityName ?? nobody, true)
|
||||||
.AddField(GetText(strs.changes_of_heart), $"{wi.AffinityCount} - \"the {affInfo}\"", true)
|
.AddField(GetText(strs.changes_of_heart),
|
||||||
|
$"{wi.AffinityCount} - \"the {affInfo}\"",
|
||||||
|
true)
|
||||||
.AddField(GetText(strs.divorces), wi.DivorceCount.ToString(), true)
|
.AddField(GetText(strs.divorces), wi.DivorceCount.ToString(), true)
|
||||||
.AddField("\u200B", "\u200B", true)
|
.AddField("\u200B", "\u200B", true)
|
||||||
.AddField(GetText(strs.fans(fansList.Count)), fansStr, true)
|
.AddField(GetText(strs.fans(fansList.Count)), fansStr, true)
|
||||||
@@ -364,30 +367,27 @@ public partial class Gambling
|
|||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[Priority(0)]
|
[Priority(0)]
|
||||||
public async Task WaifuGift(string itemName, [Leftover] IUser waifu)
|
public async Task WaifuGift(MultipleWaifuItems items, [Leftover] IUser waifu)
|
||||||
{
|
{
|
||||||
if (waifu.Id == ctx.User.Id)
|
if (waifu.Id == ctx.User.Id)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var allItems = _service.GetWaifuItems();
|
var sucess = await _service.GiftWaifuAsync(ctx.User, waifu, items.Item, items.Count);
|
||||||
var item = allItems.FirstOrDefault(x => x.Name.ToLowerInvariant() == itemName.ToLowerInvariant());
|
|
||||||
if (item is null)
|
|
||||||
{
|
|
||||||
await Response().Error(strs.waifu_gift_not_exist).SendAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sucess = await _service.GiftWaifuAsync(ctx.User, waifu, item);
|
|
||||||
|
|
||||||
if (sucess)
|
if (sucess)
|
||||||
{
|
{
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm(strs.waifu_gift(Format.Bold(item + " " + item.ItemEmoji),
|
.Confirm(strs.waifu_gift(Format.Bold($"{GetCountString(items)}{items.Item} {items.Item.ItemEmoji}"),
|
||||||
Format.Bold(waifu.ToString())))
|
Format.Bold(waifu.ToString())))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
await Response().Error(strs.not_enough(CurrencySign)).SendAsync();
|
await Response().Error(strs.not_enough(CurrencySign)).SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetCountString(MultipleWaifuItems items)
|
||||||
|
=> items.Count > 1
|
||||||
|
? $"{items.Count}x "
|
||||||
|
: string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -7,6 +7,7 @@ using NadekoBot.Db;
|
|||||||
using NadekoBot.Db.Models;
|
using NadekoBot.Db.Models;
|
||||||
using NadekoBot.Modules.Gambling.Common;
|
using NadekoBot.Modules.Gambling.Common;
|
||||||
using NadekoBot.Modules.Gambling.Common.Waifu;
|
using NadekoBot.Modules.Gambling.Common.Waifu;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Gambling.Services;
|
namespace NadekoBot.Modules.Gambling.Services;
|
||||||
|
|
||||||
@@ -89,9 +90,14 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
if (waifu is null)
|
if (waifu is null)
|
||||||
return settings.Waifu.MinPrice;
|
return settings.Waifu.MinPrice;
|
||||||
|
|
||||||
var divorces = uow.Set<WaifuUpdate>().Count(x
|
var divorces = uow.Set<WaifuUpdate>()
|
||||||
=> x.Old != null && x.Old.UserId == user.Id && x.UpdateType == WaifuUpdateType.Claimed && x.New == null);
|
.Count(x
|
||||||
var affs = uow.Set<WaifuUpdate>().AsQueryable()
|
=> x.Old != null
|
||||||
|
&& x.Old.UserId == user.Id
|
||||||
|
&& x.UpdateType == WaifuUpdateType.Claimed
|
||||||
|
&& x.New == null);
|
||||||
|
var affs = uow.Set<WaifuUpdate>()
|
||||||
|
.AsQueryable()
|
||||||
.Where(w => w.User.UserId == user.Id
|
.Where(w => w.User.UserId == user.Id
|
||||||
&& w.UpdateType == WaifuUpdateType.AffinityChanged
|
&& w.UpdateType == WaifuUpdateType.AffinityChanged
|
||||||
&& w.New != null)
|
&& w.New != null)
|
||||||
@@ -110,12 +116,14 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
if (!await _cs.RemoveAsync(user.Id, price, new("waifu", "reset")))
|
if (!await _cs.RemoveAsync(user.Id, price, new("waifu", "reset")))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var affs = uow.Set<WaifuUpdate>().AsQueryable()
|
var affs = uow.Set<WaifuUpdate>()
|
||||||
|
.AsQueryable()
|
||||||
.Where(w => w.User.UserId == user.Id
|
.Where(w => w.User.UserId == user.Id
|
||||||
&& w.UpdateType == WaifuUpdateType.AffinityChanged
|
&& w.UpdateType == WaifuUpdateType.AffinityChanged
|
||||||
&& w.New != null);
|
&& w.New != null);
|
||||||
|
|
||||||
var divorces = uow.Set<WaifuUpdate>().AsQueryable()
|
var divorces = uow.Set<WaifuUpdate>()
|
||||||
|
.AsQueryable()
|
||||||
.Where(x => x.Old != null
|
.Where(x => x.Old != null
|
||||||
&& x.Old.UserId == user.Id
|
&& x.Old.UserId == user.Id
|
||||||
&& x.UpdateType == WaifuUpdateType.Claimed
|
&& x.UpdateType == WaifuUpdateType.Claimed
|
||||||
@@ -158,14 +166,16 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
result = WaifuClaimResult.NotEnoughFunds;
|
result = WaifuClaimResult.NotEnoughFunds;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
uow.Set<WaifuInfo>().Add(w = new()
|
uow.Set<WaifuInfo>()
|
||||||
|
.Add(w = new()
|
||||||
{
|
{
|
||||||
Waifu = waifu,
|
Waifu = waifu,
|
||||||
Claimer = claimer,
|
Claimer = claimer,
|
||||||
Affinity = null,
|
Affinity = null,
|
||||||
Price = amount
|
Price = amount
|
||||||
});
|
});
|
||||||
uow.Set<WaifuUpdate>().Add(new()
|
uow.Set<WaifuUpdate>()
|
||||||
|
.Add(new()
|
||||||
{
|
{
|
||||||
User = waifu,
|
User = waifu,
|
||||||
Old = null,
|
Old = null,
|
||||||
@@ -186,7 +196,8 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
w.Price = amount + (amount / 4);
|
w.Price = amount + (amount / 4);
|
||||||
result = WaifuClaimResult.Success;
|
result = WaifuClaimResult.Success;
|
||||||
|
|
||||||
uow.Set<WaifuUpdate>().Add(new()
|
uow.Set<WaifuUpdate>()
|
||||||
|
.Add(new()
|
||||||
{
|
{
|
||||||
User = w.Waifu,
|
User = w.Waifu,
|
||||||
Old = oldClaimer,
|
Old = oldClaimer,
|
||||||
@@ -206,7 +217,8 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
w.Price = amount;
|
w.Price = amount;
|
||||||
result = WaifuClaimResult.Success;
|
result = WaifuClaimResult.Success;
|
||||||
|
|
||||||
uow.Set<WaifuUpdate>().Add(new()
|
uow.Set<WaifuUpdate>()
|
||||||
|
.Add(new()
|
||||||
{
|
{
|
||||||
User = w.Waifu,
|
User = w.Waifu,
|
||||||
Old = oldClaimer,
|
Old = oldClaimer,
|
||||||
@@ -248,7 +260,8 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
else if (w is null)
|
else if (w is null)
|
||||||
{
|
{
|
||||||
var thisUser = uow.GetOrCreateUser(user);
|
var thisUser = uow.GetOrCreateUser(user);
|
||||||
uow.Set<WaifuInfo>().Add(new()
|
uow.Set<WaifuInfo>()
|
||||||
|
.Add(new()
|
||||||
{
|
{
|
||||||
Affinity = newAff,
|
Affinity = newAff,
|
||||||
Waifu = thisUser,
|
Waifu = thisUser,
|
||||||
@@ -257,7 +270,8 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
});
|
});
|
||||||
success = true;
|
success = true;
|
||||||
|
|
||||||
uow.Set<WaifuUpdate>().Add(new()
|
uow.Set<WaifuUpdate>()
|
||||||
|
.Add(new()
|
||||||
{
|
{
|
||||||
User = thisUser,
|
User = thisUser,
|
||||||
Old = null,
|
Old = null,
|
||||||
@@ -272,7 +286,8 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
w.Affinity = newAff;
|
w.Affinity = newAff;
|
||||||
success = true;
|
success = true;
|
||||||
|
|
||||||
uow.Set<WaifuUpdate>().Add(new()
|
uow.Set<WaifuUpdate>()
|
||||||
|
.Add(new()
|
||||||
{
|
{
|
||||||
User = w.Waifu,
|
User = w.Waifu,
|
||||||
Old = oldAff,
|
Old = oldAff,
|
||||||
@@ -343,7 +358,8 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
var oldClaimer = w.Claimer;
|
var oldClaimer = w.Claimer;
|
||||||
w.Claimer = null;
|
w.Claimer = null;
|
||||||
|
|
||||||
uow.Set<WaifuUpdate>().Add(new()
|
uow.Set<WaifuUpdate>()
|
||||||
|
.Add(new()
|
||||||
{
|
{
|
||||||
User = w.Waifu,
|
User = w.Waifu,
|
||||||
Old = oldClaimer,
|
Old = oldClaimer,
|
||||||
@@ -358,16 +374,29 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
return (w, result, amount, remaining);
|
return (w, result, amount, remaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> GiftWaifuAsync(IUser from, IUser giftedWaifu, WaifuItemModel itemObj)
|
public async Task<bool> GiftWaifuAsync(
|
||||||
|
IUser from,
|
||||||
|
IUser giftedWaifu,
|
||||||
|
WaifuItemModel itemObj,
|
||||||
|
int count)
|
||||||
{
|
{
|
||||||
if (!await _cs.RemoveAsync(from, itemObj.Price, new("waifu", "item")))
|
ArgumentOutOfRangeException.ThrowIfLessThan(count, 1, nameof(count));
|
||||||
|
|
||||||
|
if (!await _cs.RemoveAsync(from, itemObj.Price * count, new("waifu", "item")))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
var totalValue = itemObj.Price * count;
|
||||||
|
|
||||||
await using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
var w = uow.Set<WaifuInfo>().ByWaifuUserId(giftedWaifu.Id, set => set.Include(x => x.Items).Include(x => x.Claimer));
|
var w = uow.Set<WaifuInfo>()
|
||||||
|
.ByWaifuUserId(giftedWaifu.Id,
|
||||||
|
set => set
|
||||||
|
.Include(x => x.Items)
|
||||||
|
.Include(x => x.Claimer));
|
||||||
if (w is null)
|
if (w is null)
|
||||||
{
|
{
|
||||||
uow.Set<WaifuInfo>().Add(w = new()
|
uow.Set<WaifuInfo>()
|
||||||
|
.Add(w = new()
|
||||||
{
|
{
|
||||||
Affinity = null,
|
Affinity = null,
|
||||||
Claimer = null,
|
Claimer = null,
|
||||||
@@ -378,20 +407,21 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
|
|
||||||
if (!itemObj.Negative)
|
if (!itemObj.Negative)
|
||||||
{
|
{
|
||||||
w.Items.Add(new()
|
w.Items.AddRange(Enumerable.Range(0, count)
|
||||||
|
.Select((_) => new WaifuItem()
|
||||||
{
|
{
|
||||||
Name = itemObj.Name.ToLowerInvariant(),
|
Name = itemObj.Name.ToLowerInvariant(),
|
||||||
ItemEmoji = itemObj.ItemEmoji
|
ItemEmoji = itemObj.ItemEmoji
|
||||||
});
|
}));
|
||||||
|
|
||||||
if (w.Claimer?.UserId == from.Id)
|
if (w.Claimer?.UserId == from.Id)
|
||||||
w.Price += (long)(itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect);
|
w.Price += (long)(totalValue * _gss.Data.Waifu.Multipliers.GiftEffect);
|
||||||
else
|
else
|
||||||
w.Price += itemObj.Price / 2;
|
w.Price += totalValue / 2;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
w.Price -= (long)(itemObj.Price * _gss.Data.Waifu.Multipliers.NegativeGiftEffect);
|
w.Price -= (long)(totalValue * _gss.Data.Waifu.Multipliers.NegativeGiftEffect);
|
||||||
if (w.Price < 1)
|
if (w.Price < 1)
|
||||||
w.Price = 1;
|
w.Price = 1;
|
||||||
}
|
}
|
||||||
@@ -492,6 +522,7 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static readonly TypedKey<long> _waifuDecayKey = $"waifu:last_decay";
|
private static readonly TypedKey<long> _waifuDecayKey = $"waifu:last_decay";
|
||||||
|
|
||||||
public async Task OnReadyAsync()
|
public async Task OnReadyAsync()
|
||||||
{
|
{
|
||||||
// only decay waifu values from shard 0
|
// only decay waifu values from shard 0
|
||||||
@@ -533,7 +564,6 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
{
|
{
|
||||||
Price = (long)(old.Price * multi)
|
Price = (long)(old.Price * multi)
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -557,6 +587,7 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
.Select(x => $"{x.Username}#{x.Discriminator}")
|
.Select(x => $"{x.Username}#{x.Discriminator}")
|
||||||
.ToListAsyncEF();
|
.ToListAsyncEF();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IReadOnlyCollection<string>> GetFansNames(int waifuId)
|
public async Task<IReadOnlyCollection<string>> GetFansNames(int waifuId)
|
||||||
{
|
{
|
||||||
await using var ctx = _db.GetDbContext();
|
await using var ctx = _db.GetDbContext();
|
||||||
@@ -573,7 +604,8 @@ public class WaifuService : INService, IReadyExecutor
|
|||||||
{
|
{
|
||||||
await using var ctx = _db.GetDbContext();
|
await using var ctx = _db.GetDbContext();
|
||||||
return await ctx.GetTable<WaifuItem>()
|
return await ctx.GetTable<WaifuItem>()
|
||||||
.Where(x => x.WaifuInfoId == ctx.GetTable<WaifuInfo>()
|
.Where(x => x.WaifuInfoId
|
||||||
|
== ctx.GetTable<WaifuInfo>()
|
||||||
.Where(x => x.WaifuId == waifuId)
|
.Where(x => x.WaifuId == waifuId)
|
||||||
.Select(x => x.Id)
|
.Select(x => x.Id)
|
||||||
.FirstOrDefault())
|
.FirstOrDefault())
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
#nullable disable
|
||||||
|
using NadekoBot.Modules.Gambling.Common;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Gambling;
|
||||||
|
|
||||||
|
public record class MultipleWaifuItems(int Count, WaifuItemModel Item);
|
@@ -0,0 +1,47 @@
|
|||||||
|
#nullable disable
|
||||||
|
using NadekoBot.Common.TypeReaders;
|
||||||
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Gambling;
|
||||||
|
|
||||||
|
public partial class MultipleWaifuItemsTypeReader : NadekoTypeReader<MultipleWaifuItems>
|
||||||
|
{
|
||||||
|
private readonly WaifuService _service;
|
||||||
|
|
||||||
|
[GeneratedRegex(@"(?:(?<count>\d+)[x*])?(?<item>.+)")]
|
||||||
|
private static partial Regex ItemRegex();
|
||||||
|
|
||||||
|
public MultipleWaifuItemsTypeReader(WaifuService service)
|
||||||
|
{
|
||||||
|
_service = service;
|
||||||
|
}
|
||||||
|
public override ValueTask<TypeReaderResult<MultipleWaifuItems>> ReadAsync(ICommandContext ctx, string input)
|
||||||
|
{
|
||||||
|
input = input.ToLowerInvariant();
|
||||||
|
var match = ItemRegex().Match(input);
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
return new(Discord.Commands.TypeReaderResult.FromError(CommandError.ParseFailed, "Invalid input."));
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = 1;
|
||||||
|
if (match.Groups["count"].Success)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(match.Groups["count"].Value, out count) || count < 1)
|
||||||
|
{
|
||||||
|
return new(Discord.Commands.TypeReaderResult.FromError(CommandError.ParseFailed, "Invalid count."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemName = match.Groups["item"].Value?.ToLowerInvariant();
|
||||||
|
var allItems = _service.GetWaifuItems();
|
||||||
|
var item = allItems.FirstOrDefault(x => x.Name.ToLowerInvariant() == itemName);
|
||||||
|
if (item is null)
|
||||||
|
{
|
||||||
|
return new(Discord.Commands.TypeReaderResult.FromError(CommandError.ParseFailed, "Waifu gift does not exist."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(Discord.Commands.TypeReaderResult.FromSuccess(new MultipleWaifuItems(count, item)));
|
||||||
|
}
|
||||||
|
}
|
@@ -18,7 +18,8 @@ public partial class Games
|
|||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[UserPerm(GuildPerm.ManageMessages)]
|
[UserPerm(GuildPerm.ManageMessages)]
|
||||||
public async Task Cleverbot()
|
[NoPublicBot]
|
||||||
|
public async Task CleverBot()
|
||||||
{
|
{
|
||||||
var channel = (ITextChannel)ctx.Channel;
|
var channel = (ITextChannel)ctx.Channel;
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ public partial class Games
|
|||||||
await uow.SaveChangesAsync();
|
await uow.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
await Response().Confirm(strs.cleverbot_disabled).SendAsync();
|
await Response().Confirm(strs.chatbot_disabled).SendAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ public partial class Games
|
|||||||
await uow.SaveChangesAsync();
|
await uow.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
await Response().Confirm(strs.cleverbot_enabled).SendAsync();
|
await Response().Confirm(strs.chatbot_enabled).SendAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,6 +1,5 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using NadekoBot.Common.ModuleBehaviors;
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
using NadekoBot.Db.Models;
|
|
||||||
using NadekoBot.Modules.Games.Common;
|
using NadekoBot.Modules.Games.Common;
|
||||||
using NadekoBot.Modules.Games.Common.ChatterBot;
|
using NadekoBot.Modules.Games.Common.ChatterBot;
|
||||||
using NadekoBot.Modules.Patronage;
|
using NadekoBot.Modules.Patronage;
|
||||||
@@ -15,43 +14,32 @@ public class ChatterBotService : IExecOnMessage
|
|||||||
public int Priority
|
public int Priority
|
||||||
=> 1;
|
=> 1;
|
||||||
|
|
||||||
private readonly FeatureLimitKey _flKey;
|
|
||||||
|
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly IPermissionChecker _perms;
|
private readonly IPermissionChecker _perms;
|
||||||
private readonly CommandHandler _cmd;
|
|
||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCredentials _creds;
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
private readonly IPatronageService _ps;
|
|
||||||
private readonly GamesConfigService _gcs;
|
private readonly GamesConfigService _gcs;
|
||||||
private readonly IMessageSenderService _sender;
|
private readonly IMessageSenderService _sender;
|
||||||
|
public readonly IPatronageService _ps;
|
||||||
|
|
||||||
public ChatterBotService(
|
public ChatterBotService(
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
IPermissionChecker perms,
|
IPermissionChecker perms,
|
||||||
IBot bot,
|
IBot bot,
|
||||||
CommandHandler cmd,
|
IPatronageService ps,
|
||||||
IHttpClientFactory factory,
|
IHttpClientFactory factory,
|
||||||
IBotCredentials creds,
|
IBotCredentials creds,
|
||||||
IPatronageService ps,
|
|
||||||
GamesConfigService gcs,
|
GamesConfigService gcs,
|
||||||
IMessageSenderService sender)
|
IMessageSenderService sender)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
_perms = perms;
|
_perms = perms;
|
||||||
_cmd = cmd;
|
|
||||||
_creds = creds;
|
_creds = creds;
|
||||||
_sender = sender;
|
_sender = sender;
|
||||||
_httpFactory = factory;
|
_httpFactory = factory;
|
||||||
_ps = ps;
|
|
||||||
_perms = perms;
|
_perms = perms;
|
||||||
_gcs = gcs;
|
_gcs = gcs;
|
||||||
|
_ps = ps;
|
||||||
_flKey = new FeatureLimitKey()
|
|
||||||
{
|
|
||||||
Key = CleverBotResponseStr.CLEVERBOT_RESPONSE,
|
|
||||||
PrettyName = "Cleverbot Replies"
|
|
||||||
};
|
|
||||||
|
|
||||||
ChatterBotGuilds = new(bot.AllGuildConfigs
|
ChatterBotGuilds = new(bot.AllGuildConfigs
|
||||||
.Where(gc => gc.CleverbotEnabled)
|
.Where(gc => gc.CleverbotEnabled)
|
||||||
@@ -69,37 +57,39 @@ public class ChatterBotService : IExecOnMessage
|
|||||||
|
|
||||||
Log.Information("Cleverbot will not work as the api key is missing");
|
Log.Information("Cleverbot will not work as the api key is missing");
|
||||||
return null;
|
return null;
|
||||||
case ChatBotImplementation.Gpt3:
|
case ChatBotImplementation.OpenAi:
|
||||||
|
var data = _gcs.Data;
|
||||||
if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey))
|
if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey))
|
||||||
return new OfficialGpt3Session(_creds.Gpt3ApiKey,
|
return new OpenAiApiSession(
|
||||||
_gcs.Data.ChatGpt.ModelName,
|
data.ChatGpt.ApiUrl,
|
||||||
_gcs.Data.ChatGpt.ChatHistory,
|
_creds.Gpt3ApiKey,
|
||||||
_gcs.Data.ChatGpt.MaxTokens,
|
data.ChatGpt.ModelName,
|
||||||
_gcs.Data.ChatGpt.MinTokens,
|
data.ChatGpt.ChatHistory,
|
||||||
_gcs.Data.ChatGpt.PersonalityPrompt,
|
data.ChatGpt.MaxTokens,
|
||||||
|
data.ChatGpt.MinTokens,
|
||||||
|
data.ChatGpt.PersonalityPrompt,
|
||||||
_client.CurrentUser.Username,
|
_client.CurrentUser.Username,
|
||||||
_httpFactory);
|
_httpFactory);
|
||||||
|
|
||||||
Log.Information("Gpt3 will not work as the api key is missing");
|
Log.Information("Openai Api will likely not work as the api key is missing");
|
||||||
return null;
|
return null;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot)
|
public IChatterBotSession GetOrCreateSession(ulong guildId)
|
||||||
{
|
{
|
||||||
var channel = msg.Channel as ITextChannel;
|
if (ChatterBotGuilds.TryGetValue(guildId, out var lazyChatBot))
|
||||||
cleverbot = null;
|
return lazyChatBot.Value;
|
||||||
|
|
||||||
if (channel is null)
|
lazyChatBot = new(() => CreateSession(), true);
|
||||||
return null;
|
ChatterBotGuilds.TryAdd(guildId, lazyChatBot);
|
||||||
|
return lazyChatBot.Value;
|
||||||
if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out var lazyCleverbot))
|
}
|
||||||
return null;
|
|
||||||
|
|
||||||
cleverbot = lazyCleverbot.Value;
|
|
||||||
|
|
||||||
|
public string PrepareMessage(IUserMessage msg)
|
||||||
|
{
|
||||||
var nadekoId = _client.CurrentUser.Id;
|
var nadekoId = _client.CurrentUser.Id;
|
||||||
var normalMention = $"<@{nadekoId}> ";
|
var normalMention = $"<@{nadekoId}> ";
|
||||||
var nickMention = $"<@!{nadekoId}> ";
|
var nickMention = $"<@!{nadekoId}> ";
|
||||||
@@ -108,6 +98,8 @@ public class ChatterBotService : IExecOnMessage
|
|||||||
message = msg.Content[normalMention.Length..].Trim();
|
message = msg.Content[normalMention.Length..].Trim();
|
||||||
else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture))
|
else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture))
|
||||||
message = msg.Content[nickMention.Length..].Trim();
|
message = msg.Content[nickMention.Length..].Trim();
|
||||||
|
else if (msg.ReferencedMessage?.Author.Id == nadekoId)
|
||||||
|
message = msg.Content;
|
||||||
else
|
else
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -119,13 +111,31 @@ public class ChatterBotService : IExecOnMessage
|
|||||||
if (guild is not SocketGuild sg)
|
if (guild is not SocketGuild sg)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
try
|
var channel = usrMsg.Channel as ITextChannel;
|
||||||
{
|
if (channel is null)
|
||||||
var message = PrepareMessage(usrMsg, out var cbs);
|
|
||||||
if (message is null || cbs is null)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var res = await _perms.CheckPermsAsync(sg,
|
if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out var lazyChatBot))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var chatBot = lazyChatBot.Value;
|
||||||
|
var message = PrepareMessage(usrMsg);
|
||||||
|
if (message is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return await RunChatterBot(sg, usrMsg, channel, chatBot, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> RunChatterBot(
|
||||||
|
SocketGuild guild,
|
||||||
|
IUserMessage usrMsg,
|
||||||
|
ITextChannel channel,
|
||||||
|
IChatterBotSession chatBot,
|
||||||
|
string message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var res = await _perms.CheckPermsAsync(guild,
|
||||||
usrMsg.Channel,
|
usrMsg.Channel,
|
||||||
usrMsg.Author,
|
usrMsg.Author,
|
||||||
CleverBotResponseStr.CLEVERBOT_RESPONSE,
|
CleverBotResponseStr.CLEVERBOT_RESPONSE,
|
||||||
@@ -134,59 +144,33 @@ public class ChatterBotService : IExecOnMessage
|
|||||||
if (!res.IsAllowed)
|
if (!res.IsAllowed)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var channel = (ITextChannel)usrMsg.Channel;
|
if (!await _ps.LimitHitAsync(LimitedFeatureName.ChatBot, usrMsg.Author.Id, 2048 / 2))
|
||||||
var conf = _ps.GetConfig();
|
|
||||||
if (!_creds.IsOwner(sg.OwnerId) && conf.IsEnabled)
|
|
||||||
{
|
{
|
||||||
var quota = await _ps.TryGetFeatureLimitAsync(_flKey, sg.OwnerId, 0);
|
// limit exceeded
|
||||||
|
return false;
|
||||||
uint? daily = quota.Quota is int dVal and < 0
|
|
||||||
? (uint)-dVal
|
|
||||||
: null;
|
|
||||||
|
|
||||||
uint? monthly = quota.Quota is int mVal and >= 0
|
|
||||||
? (uint)mVal
|
|
||||||
: null;
|
|
||||||
|
|
||||||
var maybeLimit = await _ps.TryIncrementQuotaCounterAsync(sg.OwnerId,
|
|
||||||
sg.OwnerId == usrMsg.Author.Id,
|
|
||||||
FeatureType.Limit,
|
|
||||||
_flKey.Key,
|
|
||||||
null,
|
|
||||||
daily,
|
|
||||||
monthly);
|
|
||||||
|
|
||||||
if (maybeLimit.TryPickT1(out var ql, out var counters))
|
|
||||||
{
|
|
||||||
if (ql.Quota == 0)
|
|
||||||
{
|
|
||||||
await _sender.Response(channel)
|
|
||||||
.Error(null,
|
|
||||||
text:
|
|
||||||
"In order to use the cleverbot feature, the owner of this server should be [Patron Tier X](https://patreon.com/join/nadekobot) on patreon.",
|
|
||||||
footer:
|
|
||||||
"You may disable the cleverbot feature, and this message via '.cleverbot' command")
|
|
||||||
.SendAsync();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _sender.Response(channel)
|
|
||||||
.Error(
|
|
||||||
null!,
|
|
||||||
$"You've reached your quota limit of **{ql.Quota}** responses {ql.QuotaPeriod.ToFullName()} for the cleverbot feature.",
|
|
||||||
footer: "You may wait for the quota reset or .")
|
|
||||||
.SendAsync();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = channel.TriggerTypingAsync();
|
_ = channel.TriggerTypingAsync();
|
||||||
var response = await cbs.Think(message, usrMsg.Author.ToString());
|
var response = await chatBot.Think(message, usrMsg.Author.ToString());
|
||||||
|
|
||||||
|
if (response.TryPickT0(out var result, out var error))
|
||||||
|
{
|
||||||
|
// calculate the diff in case we overestimated user's usage
|
||||||
|
var inTokens = (result.TokensIn - 2048) / 2;
|
||||||
|
|
||||||
|
// add the output tokens to the limit
|
||||||
|
await _ps.LimitForceHit(LimitedFeatureName.ChatBot,
|
||||||
|
usrMsg.Author.Id,
|
||||||
|
(inTokens) + (result.TokensOut / 2 * 3));
|
||||||
|
|
||||||
await _sender.Response(channel)
|
await _sender.Response(channel)
|
||||||
.Confirm(response)
|
.Confirm(result.Text)
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.Warning("Error in chatterbot: {Error}", error.Value);
|
||||||
|
}
|
||||||
|
|
||||||
Log.Information("""
|
Log.Information("""
|
||||||
CleverBot Executed
|
CleverBot Executed
|
9
src/NadekoBot/Modules/Games/ChatterBot/_common/Choice.cs
Normal file
9
src/NadekoBot/Modules/Games/ChatterBot/_common/Choice.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||||
|
|
||||||
|
public class Choice
|
||||||
|
{
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public Message Message { get; init; }
|
||||||
|
}
|
@@ -1,46 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
|
||||||
|
|
||||||
public class Gpt3Response
|
|
||||||
{
|
|
||||||
[JsonPropertyName("choices")]
|
|
||||||
public Choice[] Choices { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Choice
|
|
||||||
{
|
|
||||||
[JsonPropertyName("message")]
|
|
||||||
public Message Message { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Message {
|
|
||||||
[JsonPropertyName("content")]
|
|
||||||
public string Content { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Gpt3ApiRequest
|
|
||||||
{
|
|
||||||
[JsonPropertyName("model")]
|
|
||||||
public string Model { get; init; }
|
|
||||||
|
|
||||||
[JsonPropertyName("messages")]
|
|
||||||
public List<GPTMessage> Messages { get; init; }
|
|
||||||
|
|
||||||
[JsonPropertyName("temperature")]
|
|
||||||
public int Temperature { get; init; }
|
|
||||||
|
|
||||||
[JsonPropertyName("max_tokens")]
|
|
||||||
public int MaxTokens { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GPTMessage
|
|
||||||
{
|
|
||||||
[JsonPropertyName("role")]
|
|
||||||
public string Role {get; init;}
|
|
||||||
[JsonPropertyName("content")]
|
|
||||||
public string Content {get; init;}
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string Name {get; init;}
|
|
||||||
}
|
|
@@ -1,7 +1,10 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||||
|
|
||||||
public interface IChatterBotSession
|
public interface IChatterBotSession
|
||||||
{
|
{
|
||||||
Task<string> Think(string input, string username);
|
Task<OneOf<ThinkResult, Error<string>>> Think(string input, string username);
|
||||||
}
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||||
|
|
||||||
|
public class Message {
|
||||||
|
[JsonPropertyName("content")]
|
||||||
|
public string Content { get; init; }
|
||||||
|
}
|
@@ -1,5 +1,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using OneOf;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||||
|
|
||||||
@@ -18,7 +20,7 @@ public class OfficialCleverbotSession : IChatterBotSession
|
|||||||
_httpFactory = factory;
|
_httpFactory = factory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> Think(string input, string username)
|
public async Task<OneOf<ThinkResult, Error<string>>> Think(string input, string username)
|
||||||
{
|
{
|
||||||
using var http = _httpFactory.CreateClient();
|
using var http = _httpFactory.CreateClient();
|
||||||
var dataString = await http.GetStringAsync(string.Format(QueryString, input, cs ?? ""));
|
var dataString = await http.GetStringAsync(string.Format(QueryString, input, cs ?? ""));
|
||||||
@@ -27,12 +29,17 @@ public class OfficialCleverbotSession : IChatterBotSession
|
|||||||
var data = JsonConvert.DeserializeObject<CleverbotResponse>(dataString);
|
var data = JsonConvert.DeserializeObject<CleverbotResponse>(dataString);
|
||||||
|
|
||||||
cs = data?.Cs;
|
cs = data?.Cs;
|
||||||
return data?.Output;
|
return new ThinkResult
|
||||||
|
{
|
||||||
|
Text = data?.Output,
|
||||||
|
TokensIn = 2,
|
||||||
|
TokensOut = 1
|
||||||
|
};
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
Log.Warning("Unexpected cleverbot response received: {ResponseString}", dataString);
|
Log.Warning("Unexpected response from CleverBot: {ResponseString}", dataString);
|
||||||
return null;
|
return new Error<string>("Unexpected CleverBot response received");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,105 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System.Net.Http.Json;
|
|
||||||
using SharpToken;
|
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
|
||||||
|
|
||||||
public class OfficialGpt3Session : IChatterBotSession
|
|
||||||
{
|
|
||||||
private string Uri
|
|
||||||
=> $"https://api.openai.com/v1/chat/completions";
|
|
||||||
|
|
||||||
private readonly string _apiKey;
|
|
||||||
private readonly string _model;
|
|
||||||
private readonly int _maxHistory;
|
|
||||||
private readonly int _maxTokens;
|
|
||||||
private readonly int _minTokens;
|
|
||||||
private readonly string _nadekoUsername;
|
|
||||||
private readonly GptEncoding _encoding;
|
|
||||||
private List<GPTMessage> messages = new();
|
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public OfficialGpt3Session(
|
|
||||||
string apiKey,
|
|
||||||
ChatGptModel model,
|
|
||||||
int chatHistory,
|
|
||||||
int maxTokens,
|
|
||||||
int minTokens,
|
|
||||||
string personality,
|
|
||||||
string nadekoUsername,
|
|
||||||
IHttpClientFactory factory)
|
|
||||||
{
|
|
||||||
_apiKey = apiKey;
|
|
||||||
_httpFactory = factory;
|
|
||||||
switch (model)
|
|
||||||
{
|
|
||||||
case ChatGptModel.Gpt35Turbo:
|
|
||||||
_model = "gpt-3.5-turbo";
|
|
||||||
break;
|
|
||||||
case ChatGptModel.Gpt4:
|
|
||||||
_model = "gpt-4";
|
|
||||||
break;
|
|
||||||
case ChatGptModel.Gpt432k:
|
|
||||||
_model = "gpt-4-32k";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_maxHistory = chatHistory;
|
|
||||||
_maxTokens = maxTokens;
|
|
||||||
_minTokens = minTokens;
|
|
||||||
_nadekoUsername = nadekoUsername;
|
|
||||||
_encoding = GptEncoding.GetEncodingForModel(_model);
|
|
||||||
messages.Add(new GPTMessage(){Role = "user", Content = personality, Name = _nadekoUsername});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> Think(string input, string username)
|
|
||||||
{
|
|
||||||
messages.Add(new GPTMessage(){Role = "user", Content = input, Name = username});
|
|
||||||
while(messages.Count > _maxHistory + 2){
|
|
||||||
messages.RemoveAt(1);
|
|
||||||
}
|
|
||||||
int tokensUsed = 0;
|
|
||||||
foreach(GPTMessage message in messages){
|
|
||||||
tokensUsed += _encoding.Encode(message.Content).Count;
|
|
||||||
}
|
|
||||||
tokensUsed *= 2; //Unsure why this is the case, but the token count chatgpt reports back is double what I calculate.
|
|
||||||
//check if we have the minimum number of tokens available to use. Remove messages until we have enough, otherwise exit out and inform the user why.
|
|
||||||
while(_maxTokens - tokensUsed <= _minTokens){
|
|
||||||
if(messages.Count > 2){
|
|
||||||
int tokens = _encoding.Encode(messages[1].Content).Count * 2;
|
|
||||||
tokensUsed -= tokens;
|
|
||||||
messages.RemoveAt(1);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
return "Token count exceeded, please increase the number of tokens in the bot config and restart.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
using var http = _httpFactory.CreateClient();
|
|
||||||
http.DefaultRequestHeaders.Authorization = new("Bearer", _apiKey);
|
|
||||||
var data = await http.PostAsJsonAsync(Uri, new Gpt3ApiRequest()
|
|
||||||
{
|
|
||||||
Model = _model,
|
|
||||||
Messages = messages,
|
|
||||||
MaxTokens = _maxTokens - tokensUsed,
|
|
||||||
Temperature = 1,
|
|
||||||
});
|
|
||||||
var dataString = await data.Content.ReadAsStringAsync();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var response = JsonConvert.DeserializeObject<Gpt3Response>(dataString);
|
|
||||||
string message = response?.Choices[0]?.Message?.Content;
|
|
||||||
//Can't rely on the return to except, now that we need to add it to the messages list.
|
|
||||||
_ = message ?? throw new ArgumentNullException(nameof(message));
|
|
||||||
messages.Add(new GPTMessage(){Role = "assistant", Content = message, Name = _nadekoUsername});
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
Log.Warning("Unexpected GPT-3 response received: {ResponseString}", dataString);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||||
|
|
||||||
|
public class OpenAiApiMessage
|
||||||
|
{
|
||||||
|
[JsonPropertyName("role")]
|
||||||
|
public string Role { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("content")]
|
||||||
|
public string Content { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; init; }
|
||||||
|
}
|
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||||
|
|
||||||
|
public class OpenAiApiRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("model")]
|
||||||
|
public string Model { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("messages")]
|
||||||
|
public List<OpenAiApiMessage> Messages { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("temperature")]
|
||||||
|
public int Temperature { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("max_tokens")]
|
||||||
|
public int MaxTokens { get; init; }
|
||||||
|
}
|
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||||
|
|
||||||
|
public class OpenAiApiUsageData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("prompt_tokens")]
|
||||||
|
public int PromptTokens { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("completion_tokens")]
|
||||||
|
public int CompletionTokens { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("total_tokens")]
|
||||||
|
public int TotalTokens { get; set; }
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
#nullable disable
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||||
|
|
||||||
|
public class OpenAiCompletionResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("choices")]
|
||||||
|
public Choice[] Choices { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("usage")]
|
||||||
|
public OpenAiApiUsageData Usage { get; set; }
|
||||||
|
}
|
@@ -0,0 +1,147 @@
|
|||||||
|
#nullable disable
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using OneOf.Types;
|
||||||
|
using SharpToken;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||||
|
|
||||||
|
public partial class OpenAiApiSession : IChatterBotSession
|
||||||
|
{
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
private readonly string _apiKey;
|
||||||
|
private readonly string _model;
|
||||||
|
private readonly int _maxHistory;
|
||||||
|
private readonly int _maxTokens;
|
||||||
|
private readonly int _minTokens;
|
||||||
|
private readonly string _nadekoUsername;
|
||||||
|
private readonly GptEncoding _encoding;
|
||||||
|
private List<OpenAiApiMessage> messages = new();
|
||||||
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
|
|
||||||
|
|
||||||
|
public OpenAiApiSession(
|
||||||
|
string url,
|
||||||
|
string apiKey,
|
||||||
|
string model,
|
||||||
|
int chatHistory,
|
||||||
|
int maxTokens,
|
||||||
|
int minTokens,
|
||||||
|
string personality,
|
||||||
|
string nadekoUsername,
|
||||||
|
IHttpClientFactory factory)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out _))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Invalid OpenAi api url provided", nameof(url));
|
||||||
|
}
|
||||||
|
|
||||||
|
_baseUrl = url.TrimEnd('/');
|
||||||
|
|
||||||
|
_apiKey = apiKey;
|
||||||
|
_model = model;
|
||||||
|
_httpFactory = factory;
|
||||||
|
_maxHistory = chatHistory;
|
||||||
|
_maxTokens = maxTokens;
|
||||||
|
_minTokens = minTokens;
|
||||||
|
_nadekoUsername = UsernameCleaner().Replace(nadekoUsername, "");
|
||||||
|
_encoding = GptEncoding.GetEncodingForModel("gpt-4o");
|
||||||
|
if (!string.IsNullOrWhiteSpace(personality))
|
||||||
|
{
|
||||||
|
messages.Add(new()
|
||||||
|
{
|
||||||
|
Role = "system",
|
||||||
|
Content = personality,
|
||||||
|
Name = _nadekoUsername
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[GeneratedRegex("[^a-zA-Z0-9_-]")]
|
||||||
|
private static partial Regex UsernameCleaner();
|
||||||
|
|
||||||
|
public async Task<OneOf.OneOf<ThinkResult, Error<string>>> Think(string input, string username)
|
||||||
|
{
|
||||||
|
username = UsernameCleaner().Replace(username, "");
|
||||||
|
|
||||||
|
messages.Add(new()
|
||||||
|
{
|
||||||
|
Role = "user",
|
||||||
|
Content = input,
|
||||||
|
Name = username
|
||||||
|
});
|
||||||
|
|
||||||
|
while (messages.Count > _maxHistory + 2)
|
||||||
|
{
|
||||||
|
messages.RemoveAt(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokensUsed = messages.Sum(message => _encoding.Encode(message.Content).Count);
|
||||||
|
|
||||||
|
tokensUsed *= 2;
|
||||||
|
|
||||||
|
//check if we have the minimum number of tokens available to use. Remove messages until we have enough, otherwise exit out and inform the user why.
|
||||||
|
while (_maxTokens - tokensUsed <= _minTokens)
|
||||||
|
{
|
||||||
|
if (messages.Count > 2)
|
||||||
|
{
|
||||||
|
var tokens = _encoding.Encode(messages[1].Content).Count * 2;
|
||||||
|
tokensUsed -= tokens;
|
||||||
|
messages.RemoveAt(1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new Error<string>(
|
||||||
|
"Token count exceeded, please increase the number of tokens in the bot config and restart.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using var http = _httpFactory.CreateClient();
|
||||||
|
http.DefaultRequestHeaders.Authorization = new("Bearer", _apiKey);
|
||||||
|
|
||||||
|
var data = await http.PostAsJsonAsync($"{_baseUrl}/v1/chat/completions",
|
||||||
|
new OpenAiApiRequest()
|
||||||
|
{
|
||||||
|
Model = _model,
|
||||||
|
Messages = messages,
|
||||||
|
MaxTokens = _maxTokens - tokensUsed,
|
||||||
|
Temperature = 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
var dataString = await data.Content.ReadAsStringAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = JsonConvert.DeserializeObject<OpenAiCompletionResponse>(dataString);
|
||||||
|
|
||||||
|
// Log.Information("Received response: {Response} ", dataString);
|
||||||
|
var res = response?.Choices?[0];
|
||||||
|
var message = res?.Message?.Content;
|
||||||
|
|
||||||
|
if (message is null)
|
||||||
|
{
|
||||||
|
return new Error<string>("ChatGpt: Received no response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.Add(new()
|
||||||
|
{
|
||||||
|
Role = "assistant",
|
||||||
|
Content = message,
|
||||||
|
Name = _nadekoUsername
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ThinkResult()
|
||||||
|
{
|
||||||
|
Text = message,
|
||||||
|
TokensIn = response.Usage.PromptTokens,
|
||||||
|
TokensOut = response.Usage.CompletionTokens
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Log.Warning("Unexpected response received from OpenAI: {ResponseString}", dataString);
|
||||||
|
return new Error<string>("Unexpected response received");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
#nullable disable
|
||||||
|
using System.CodeDom;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||||
|
|
||||||
|
public sealed class ThinkResult
|
||||||
|
{
|
||||||
|
public string Text { get; set; }
|
||||||
|
public int TokensIn { get; set; }
|
||||||
|
public int TokensOut { get; set; }
|
||||||
|
}
|
@@ -8,7 +8,7 @@ namespace NadekoBot.Modules.Games.Common;
|
|||||||
public sealed partial class GamesConfig : ICloneable<GamesConfig>
|
public sealed partial class GamesConfig : ICloneable<GamesConfig>
|
||||||
{
|
{
|
||||||
[Comment("DO NOT CHANGE")]
|
[Comment("DO NOT CHANGE")]
|
||||||
public int Version { get; set; } = 3;
|
public int Version { get; set; } = 5;
|
||||||
|
|
||||||
[Comment("Hangman related settings (.hangman command)")]
|
[Comment("Hangman related settings (.hangman command)")]
|
||||||
public HangmanConfig Hangman { get; set; } = new()
|
public HangmanConfig Hangman { get; set; } = new()
|
||||||
@@ -103,10 +103,13 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
[Comment(@"Which chatbot API should bot use.
|
[Comment(
|
||||||
|
"""
|
||||||
|
Which chatbot API should bot use.
|
||||||
'cleverbot' - bot will use Cleverbot API.
|
'cleverbot' - bot will use Cleverbot API.
|
||||||
'gpt3' - bot will use GPT-3 API")]
|
'openai' - bot will use OpenAi API
|
||||||
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt3;
|
""")]
|
||||||
|
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.OpenAi;
|
||||||
|
|
||||||
public ChatGptConfig ChatGpt { get; set; } = new();
|
public ChatGptConfig ChatGpt { get; set; } = new();
|
||||||
}
|
}
|
||||||
@@ -114,22 +117,41 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
|
|||||||
[Cloneable]
|
[Cloneable]
|
||||||
public sealed partial class ChatGptConfig
|
public sealed partial class ChatGptConfig
|
||||||
{
|
{
|
||||||
[Comment(@"Which GPT-3 Model should bot use.
|
[Comment("""
|
||||||
gpt35turbo - cheapest
|
Url to any openai api compatible url.
|
||||||
gpt4 - 30x more expensive, higher quality
|
Make sure to modify the modelName appropriately
|
||||||
gp432k - same model as above, but with a 32k token limit")]
|
DO NOT add /v1/chat/completions suffix to the url
|
||||||
public ChatGptModel ModelName { get; set; } = ChatGptModel.Gpt35Turbo;
|
""")]
|
||||||
|
public string ApiUrl { get; set; } = "https://api.openai.com";
|
||||||
|
|
||||||
[Comment(@"How should the chat bot behave, what's its personality? (Usage of this counts towards the max tokens)")]
|
[Comment("""
|
||||||
public string PersonalityPrompt { get; set; } = "You are a chat bot willing to have a conversation with anyone about anything.";
|
Which GPT Model should bot use.
|
||||||
|
gpt-3.5-turbo - cheapest
|
||||||
|
gpt-4o - more expensive, higher quality
|
||||||
|
|
||||||
[Comment(@"The maximum number of messages in a conversation that can be remembered. (This will increase the number of tokens used)")]
|
If you are using another openai compatible api, you may use any of the models supported by that api
|
||||||
|
""")]
|
||||||
|
public string ModelName { get; set; } = "gpt-3.5-turbo";
|
||||||
|
|
||||||
|
[Comment("""
|
||||||
|
How should the chatbot behave, what's its personality?
|
||||||
|
This will be sent as a system message.
|
||||||
|
Usage of this counts towards the max tokens.
|
||||||
|
""")]
|
||||||
|
public string PersonalityPrompt { get; set; } =
|
||||||
|
"You are a chat bot willing to have a conversation with anyone about anything.";
|
||||||
|
|
||||||
|
[Comment(
|
||||||
|
"""
|
||||||
|
The maximum number of messages in a conversation that can be remembered.
|
||||||
|
This will increase the number of tokens used.
|
||||||
|
""")]
|
||||||
public int ChatHistory { get; set; } = 5;
|
public int ChatHistory { get; set; } = 5;
|
||||||
|
|
||||||
[Comment(@"The maximum number of tokens to use per GPT-3 API call")]
|
[Comment(@"The maximum number of tokens to use per OpenAi API call")]
|
||||||
public int MaxTokens { get; set; } = 100;
|
public int MaxTokens { get; set; } = 100;
|
||||||
|
|
||||||
[Comment(@"The minimum number of tokens to use per GPT-3 API call, such that chat history is removed to make room.")]
|
[Comment(@"The minimum number of tokens to use per GPT API call, such that chat history is removed to make room.")]
|
||||||
public int MinTokens { get; set; } = 30;
|
public int MinTokens { get; set; } = 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,12 +185,11 @@ public sealed partial class RaceAnimal
|
|||||||
public enum ChatBotImplementation
|
public enum ChatBotImplementation
|
||||||
{
|
{
|
||||||
Cleverbot,
|
Cleverbot,
|
||||||
Gpt3
|
OpenAi = 1,
|
||||||
}
|
|
||||||
|
|
||||||
public enum ChatGptModel
|
[Obsolete]
|
||||||
{
|
Gpt = 1,
|
||||||
Gpt35Turbo,
|
|
||||||
Gpt4,
|
[Obsolete]
|
||||||
Gpt432k
|
Gpt3 = 1,
|
||||||
}
|
}
|
@@ -32,29 +32,21 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
|
|||||||
gs => gs.ChatBot,
|
gs => gs.ChatBot,
|
||||||
ConfigParsers.InsensitiveEnum,
|
ConfigParsers.InsensitiveEnum,
|
||||||
ConfigPrinters.ToString);
|
ConfigPrinters.ToString);
|
||||||
|
|
||||||
|
AddParsedProp("gpt.apiUrl",
|
||||||
|
gs => gs.ChatGpt.ApiUrl,
|
||||||
|
ConfigParsers.String,
|
||||||
|
ConfigPrinters.ToString);
|
||||||
|
|
||||||
AddParsedProp("gpt.modelName",
|
AddParsedProp("gpt.modelName",
|
||||||
gs => gs.ChatGpt.ModelName,
|
gs => gs.ChatGpt.ModelName,
|
||||||
ConfigParsers.InsensitiveEnum,
|
ConfigParsers.String,
|
||||||
ConfigPrinters.ToString);
|
ConfigPrinters.ToString);
|
||||||
|
|
||||||
AddParsedProp("gpt.personality",
|
AddParsedProp("gpt.personality",
|
||||||
gs => gs.ChatGpt.PersonalityPrompt,
|
gs => gs.ChatGpt.PersonalityPrompt,
|
||||||
ConfigParsers.String,
|
ConfigParsers.String,
|
||||||
ConfigPrinters.ToString);
|
ConfigPrinters.ToString);
|
||||||
AddParsedProp("gpt.chathistory",
|
|
||||||
gs => gs.ChatGpt.ChatHistory,
|
|
||||||
int.TryParse,
|
|
||||||
ConfigPrinters.ToString,
|
|
||||||
val => val > 0);
|
|
||||||
AddParsedProp("gpt.max_tokens",
|
|
||||||
gs => gs.ChatGpt.MaxTokens,
|
|
||||||
int.TryParse,
|
|
||||||
ConfigPrinters.ToString,
|
|
||||||
val => val > 0);
|
|
||||||
AddParsedProp("gpt.min_tokens",
|
|
||||||
gs => gs.ChatGpt.MinTokens,
|
|
||||||
int.TryParse,
|
|
||||||
ConfigPrinters.ToString,
|
|
||||||
val => val > 0);
|
|
||||||
|
|
||||||
Migrate();
|
Migrate();
|
||||||
}
|
}
|
||||||
@@ -73,21 +65,55 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.Version < 2)
|
|
||||||
{
|
|
||||||
ModifyConfig(c =>
|
|
||||||
{
|
|
||||||
c.Version = 2;
|
|
||||||
c.ChatBot = ChatBotImplementation.Cleverbot;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.Version < 3)
|
if (data.Version < 3)
|
||||||
{
|
{
|
||||||
ModifyConfig(c =>
|
ModifyConfig(c =>
|
||||||
{
|
{
|
||||||
c.Version = 3;
|
c.Version = 3;
|
||||||
c.ChatGpt.ModelName = ChatGptModel.Gpt35Turbo;
|
c.ChatGpt.ModelName = "gpt35turbo";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.Version < 4)
|
||||||
|
{
|
||||||
|
ModifyConfig(c =>
|
||||||
|
{
|
||||||
|
c.Version = 4;
|
||||||
|
#pragma warning disable CS0612 // Type or member is obsolete
|
||||||
|
c.ChatGpt.ModelName =
|
||||||
|
c.ChatGpt.ModelName.Equals("gpt4", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| c.ChatGpt.ModelName.Equals("gpt432k", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "gpt-4o"
|
||||||
|
: "gpt-3.5-turbo";
|
||||||
|
#pragma warning restore CS0612 // Type or member is obsolete
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.Version < 5)
|
||||||
|
{
|
||||||
|
ModifyConfig(c =>
|
||||||
|
{
|
||||||
|
c.Version = 5;
|
||||||
|
c.ChatBot = c.ChatBot == ChatBotImplementation.OpenAi
|
||||||
|
? ChatBotImplementation.OpenAi
|
||||||
|
: c.ChatBot;
|
||||||
|
|
||||||
|
if (c.ChatGpt.ModelName.Equals("gpt4o", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
c.ChatGpt.ModelName = "gpt-4o";
|
||||||
|
}
|
||||||
|
else if (c.ChatGpt.ModelName.Equals("gpt35turbo", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
c.ChatGpt.ModelName = "gpt-3.5-turbo";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log.Warning(
|
||||||
|
"Unknown OpenAI api model name: {ModelName}. "
|
||||||
|
+ "It will be reset to 'gpt-3.5-turbo' only this time",
|
||||||
|
c.ChatGpt.ModelName);
|
||||||
|
c.ChatGpt.ModelName = "gpt-3.5-turbo";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -519,31 +519,10 @@ public sealed partial class Help : NadekoModule<HelpService>
|
|||||||
"https://nadekobot.readthedocs.io/en/latest/"))
|
"https://nadekobot.readthedocs.io/en/latest/"))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
|
|
||||||
|
|
||||||
private Task SelfhostAction(SocketMessageComponent smc)
|
|
||||||
=> smc.RespondConfirmAsync(_sender,
|
|
||||||
"""
|
|
||||||
- In case you don't want or cannot Donate to NadekoBot project, but you
|
|
||||||
- NadekoBot is a free and [open source](https://gitlab.com/kwoth/nadekobot) project which means you can run your own "selfhosted" instance on your computer.
|
|
||||||
|
|
||||||
*Keep in mind that running the bot on your computer means that the bot will be offline when you turn off your computer*
|
|
||||||
|
|
||||||
- You can find the selfhosting guides by using the `.guide` command and clicking on the second link that pops up.
|
|
||||||
- If you decide to selfhost the bot, still consider [supporting the project](https://patreon.com/join/nadekobot) to keep the development going :)
|
|
||||||
""",
|
|
||||||
true);
|
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[OnlyPublicBot]
|
[OnlyPublicBot]
|
||||||
public async Task Donate()
|
public async Task Donate()
|
||||||
{
|
{
|
||||||
var selfhostInter = _inter.Create(ctx.User.Id,
|
|
||||||
new ButtonBuilder(
|
|
||||||
emote: new Emoji("🖥️"),
|
|
||||||
customId: "donate:selfhosting",
|
|
||||||
label: "Selfhosting"),
|
|
||||||
SelfhostAction);
|
|
||||||
|
|
||||||
var eb = _sender.CreateEmbed()
|
var eb = _sender.CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithTitle("Thank you for considering to donate to the NadekoBot project!");
|
.WithTitle("Thank you for considering to donate to the NadekoBot project!");
|
||||||
@@ -580,7 +559,6 @@ public sealed partial class Help : NadekoModule<HelpService>
|
|||||||
await Response()
|
await Response()
|
||||||
.Channel(await ctx.User.CreateDMChannelAsync())
|
.Channel(await ctx.User.CreateDMChannelAsync())
|
||||||
.Embed(eb)
|
.Embed(eb)
|
||||||
.Interaction(selfhostInter)
|
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
|
|
||||||
_ = ctx.OkAsync();
|
_ = ctx.OkAsync();
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using NadekoBot.Modules.Music.Services;
|
using NadekoBot.Modules.Music.Services;
|
||||||
using NadekoBot.Db.Models;
|
using NadekoBot.Db.Models;
|
||||||
|
using NadekoBot.Modules.Utility;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Music;
|
namespace NadekoBot.Modules.Music;
|
||||||
|
|
||||||
|
@@ -32,5 +32,14 @@ public class PatronageConfig : ConfigServiceBase<PatronConfigData>
|
|||||||
c.IsEnabled = false;
|
c.IsEnabled = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
ModifyConfig(c =>
|
||||||
|
{
|
||||||
|
if (c.Version == 2)
|
||||||
|
{
|
||||||
|
c.Version = 3;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.EntityFrameworkCore;
|
using LinqToDB.EntityFrameworkCore;
|
||||||
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
using NadekoBot.Modules.Gambling.Services;
|
using NadekoBot.Modules.Gambling.Services;
|
||||||
using NadekoBot.Modules.Patronage;
|
using NadekoBot.Modules.Patronage;
|
||||||
using NadekoBot.Services.Currency;
|
using NadekoBot.Services.Currency;
|
||||||
@@ -8,7 +9,7 @@ using NadekoBot.Db.Models;
|
|||||||
|
|
||||||
namespace NadekoBot.Modules.Utility;
|
namespace NadekoBot.Modules.Utility;
|
||||||
|
|
||||||
public sealed class CurrencyRewardService : INService, IDisposable
|
public sealed class CurrencyRewardService : INService, IReadyExecutor
|
||||||
{
|
{
|
||||||
private readonly ICurrencyService _cs;
|
private readonly ICurrencyService _cs;
|
||||||
private readonly IPatronageService _ps;
|
private readonly IPatronageService _ps;
|
||||||
@@ -32,16 +33,14 @@ public sealed class CurrencyRewardService : INService, IDisposable
|
|||||||
_config = config;
|
_config = config;
|
||||||
_client = client;
|
_client = client;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OnReadyAsync()
|
||||||
|
{
|
||||||
_ps.OnNewPatronPayment += OnNewPayment;
|
_ps.OnNewPatronPayment += OnNewPayment;
|
||||||
_ps.OnPatronRefunded += OnPatronRefund;
|
_ps.OnPatronRefunded += OnPatronRefund;
|
||||||
_ps.OnPatronUpdated += OnPatronUpdate;
|
_ps.OnPatronUpdated += OnPatronUpdate;
|
||||||
}
|
return Task.CompletedTask;
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_ps.OnNewPatronPayment -= OnNewPayment;
|
|
||||||
_ps.OnPatronRefunded -= OnPatronRefund;
|
|
||||||
_ps.OnPatronUpdated -= OnPatronUpdate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnPatronUpdate(Patron oldPatron, Patron newPatron)
|
private async Task OnPatronUpdate(Patron oldPatron, Patron newPatron)
|
||||||
|
@@ -1,11 +0,0 @@
|
|||||||
using NadekoBot.Db.Models;
|
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Patronage;
|
|
||||||
|
|
||||||
public readonly struct InsufficientTier
|
|
||||||
{
|
|
||||||
public FeatureType FeatureType { get; init; }
|
|
||||||
public string Feature { get; init; }
|
|
||||||
public PatronTier RequiredTier { get; init; }
|
|
||||||
public PatronTier UserTier { get; init; }
|
|
||||||
}
|
|
@@ -140,7 +140,6 @@ public class PatreonClient : IDisposable
|
|||||||
LastChargeDate = m.Attributes.LastChargeDate,
|
LastChargeDate = m.Attributes.LastChargeDate,
|
||||||
LastChargeStatus = m.Attributes.LastChargeStatus
|
LastChargeStatus = m.Attributes.LastChargeStatus
|
||||||
})
|
})
|
||||||
.Where(x => x.UserId == 140788173885276160)
|
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
yield return userData;
|
yield return userData;
|
||||||
|
@@ -26,8 +26,3 @@ public sealed class PatreonMemberData : ISubscriberData
|
|||||||
_ => SubscriptionChargeStatus.Other,
|
_ => SubscriptionChargeStatus.Other,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PatreonPledgeData
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
@@ -71,17 +71,16 @@ public partial class Help
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var patron = await _service.GetPatronAsync(user.Id);
|
var maybePatron = await _service.GetPatronAsync(user.Id);
|
||||||
var quotaStats = await _service.GetUserQuotaStatistic(user.Id);
|
|
||||||
|
var quotaStats = await _service.LimitStats(user.Id);
|
||||||
|
|
||||||
var eb = _sender.CreateEmbed()
|
var eb = _sender.CreateEmbed()
|
||||||
.WithAuthor(user)
|
.WithAuthor(user)
|
||||||
.WithTitle(GetText(strs.patron_info))
|
.WithTitle(GetText(strs.patron_info))
|
||||||
.WithOkColor();
|
.WithOkColor();
|
||||||
|
|
||||||
if (quotaStats.Commands.Count == 0
|
if (quotaStats.Count == 0 || maybePatron is not { } patron)
|
||||||
&& quotaStats.Groups.Count == 0
|
|
||||||
&& quotaStats.Modules.Count == 0)
|
|
||||||
{
|
{
|
||||||
eb.WithDescription(GetText(strs.no_quota_found));
|
eb.WithDescription(GetText(strs.no_quota_found));
|
||||||
}
|
}
|
||||||
@@ -97,27 +96,10 @@ public partial class Help
|
|||||||
|
|
||||||
eb.AddField(GetText(strs.quotas), "", false);
|
eb.AddField(GetText(strs.quotas), "", false);
|
||||||
|
|
||||||
if (quotaStats.Commands.Count > 0)
|
var text = GetQuotaList(quotaStats);
|
||||||
{
|
|
||||||
var text = GetQuotaList(quotaStats.Commands);
|
|
||||||
if (!string.IsNullOrWhiteSpace(text))
|
|
||||||
eb.AddField(GetText(strs.commands), text, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (quotaStats.Groups.Count > 0)
|
|
||||||
{
|
|
||||||
var text = GetQuotaList(quotaStats.Groups);
|
|
||||||
if (!string.IsNullOrWhiteSpace(text))
|
|
||||||
eb.AddField(GetText(strs.groups), text, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (quotaStats.Modules.Count > 0)
|
|
||||||
{
|
|
||||||
var text = GetQuotaList(quotaStats.Modules);
|
|
||||||
if (!string.IsNullOrWhiteSpace(text))
|
if (!string.IsNullOrWhiteSpace(text))
|
||||||
eb.AddField(GetText(strs.modules), text, true);
|
eb.AddField(GetText(strs.modules), text, true);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -131,26 +113,28 @@ public partial class Help
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetQuotaList(IReadOnlyDictionary<string, FeatureQuotaStats> featureQuotaStats)
|
private string GetQuotaList(
|
||||||
|
IReadOnlyDictionary<LimitedFeatureName, (int Cur, QuotaLimit Quota)> featureQuotaStats)
|
||||||
{
|
{
|
||||||
var text = string.Empty;
|
var text = string.Empty;
|
||||||
foreach (var (key, q) in featureQuotaStats)
|
foreach (var (key, (cur, quota)) in featureQuotaStats)
|
||||||
{
|
{
|
||||||
text += $"\n\t`{key}`\n";
|
text += $"\n\t`{key}`\n";
|
||||||
if (q.Hourly != default)
|
if (quota.QuotaPeriod == QuotaPer.PerHour)
|
||||||
text += $" {GetEmoji(q.Hourly)} {q.Hourly.Cur}/{q.Hourly.Max} per hour\n";
|
text += $" {cur}/{(quota.Quota == -1 ? "∞" : quota.Quota)} {QuotaPeriodToString(quota.QuotaPeriod)}\n";
|
||||||
if (q.Daily != default)
|
|
||||||
text += $" {GetEmoji(q.Daily)} {q.Daily.Cur}/{q.Daily.Max} per day\n";
|
|
||||||
if (q.Monthly != default)
|
|
||||||
text += $" {GetEmoji(q.Monthly)} {q.Monthly.Cur}/{q.Monthly.Max} per month\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetEmoji((uint Cur, uint Max) limit)
|
public string QuotaPeriodToString(QuotaPer per)
|
||||||
=> limit.Cur < limit.Max
|
=> per switch
|
||||||
? "✅"
|
{
|
||||||
: "⚠️";
|
QuotaPer.PerHour => "per hour",
|
||||||
|
QuotaPer.PerDay => "per day",
|
||||||
|
QuotaPer.PerMonth => "per month",
|
||||||
|
QuotaPer.Total => "total",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(per), per, null)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -2,9 +2,8 @@
|
|||||||
using LinqToDB.EntityFrameworkCore;
|
using LinqToDB.EntityFrameworkCore;
|
||||||
using NadekoBot.Common.ModuleBehaviors;
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
using NadekoBot.Db.Models;
|
using NadekoBot.Db.Models;
|
||||||
using OneOf;
|
using StackExchange.Redis;
|
||||||
using OneOf.Types;
|
using System.Diagnostics;
|
||||||
using CommandInfo = Discord.Commands.CommandInfo;
|
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Patronage;
|
namespace NadekoBot.Modules.Patronage;
|
||||||
|
|
||||||
@@ -12,7 +11,6 @@ namespace NadekoBot.Modules.Patronage;
|
|||||||
public sealed class PatronageService
|
public sealed class PatronageService
|
||||||
: IPatronageService,
|
: IPatronageService,
|
||||||
IReadyExecutor,
|
IReadyExecutor,
|
||||||
IExecPreCommand,
|
|
||||||
INService
|
INService
|
||||||
{
|
{
|
||||||
public event Func<Patron, Task> OnNewPatronPayment = static delegate { return Task.CompletedTask; };
|
public event Func<Patron, Task> OnNewPatronPayment = static delegate { return Task.CompletedTask; };
|
||||||
@@ -60,7 +58,7 @@ public sealed class PatronageService
|
|||||||
if (_client.ShardId != 0)
|
if (_client.ShardId != 0)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
return Task.WhenAll(ResetLoopAsync(), LoadSubscribersLoopAsync());
|
return Task.WhenAll(LoadSubscribersLoopAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadSubscribersLoopAsync()
|
private async Task LoadSubscribersLoopAsync()
|
||||||
@@ -85,71 +83,6 @@ public sealed class PatronageService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ResetLoopAsync()
|
|
||||||
{
|
|
||||||
await Task.Delay(1.Minutes());
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!_pConf.Data.IsEnabled)
|
|
||||||
{
|
|
||||||
await Task.Delay(1.Minutes());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
var lastRun = DateTime.MinValue;
|
|
||||||
|
|
||||||
var result = await _cache.GetAsync(_quotaKey);
|
|
||||||
if (result.TryGetValue(out var lastVal) && lastVal != default)
|
|
||||||
{
|
|
||||||
lastRun = DateTime.FromBinary(lastVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
var nowDate = now.ToDateOnly();
|
|
||||||
var lastDate = lastRun.ToDateOnly();
|
|
||||||
|
|
||||||
await using var ctx = _db.GetDbContext();
|
|
||||||
|
|
||||||
if ((lastDate.Day == 1 || (lastDate.Month != nowDate.Month)) && nowDate.Day > 1)
|
|
||||||
{
|
|
||||||
// assumes bot won't be offline for a year
|
|
||||||
await ctx.GetTable<PatronQuota>()
|
|
||||||
.TruncateAsync();
|
|
||||||
}
|
|
||||||
else if (nowDate.DayNumber != lastDate.DayNumber)
|
|
||||||
{
|
|
||||||
// day is different, means hour is different.
|
|
||||||
// reset both hourly and daily quota counts.
|
|
||||||
await ctx.GetTable<PatronQuota>()
|
|
||||||
.UpdateAsync((old) => new()
|
|
||||||
{
|
|
||||||
HourlyCount = 0,
|
|
||||||
DailyCount = 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (now.Hour != lastRun.Hour) // if it's not, just reset hourly quotas
|
|
||||||
{
|
|
||||||
await ctx.GetTable<PatronQuota>()
|
|
||||||
.UpdateAsync((old) => new()
|
|
||||||
{
|
|
||||||
HourlyCount = 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// assumes that the code above runs in less than an hour
|
|
||||||
await _cache.AddAsync(_quotaKey, now.ToBinary());
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error(ex, "Error in quota reset loop. Message: {ErrorMessage}", ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(TimeSpan.FromHours(1).Add(TimeSpan.FromMinutes(1)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ProcesssPatronsAsync(IReadOnlyCollection<ISubscriberData> subscribersEnum)
|
private async Task ProcesssPatronsAsync(IReadOnlyCollection<ISubscriberData> subscribersEnum)
|
||||||
{
|
{
|
||||||
// process only users who have discord accounts connected
|
// process only users who have discord accounts connected
|
||||||
@@ -203,7 +136,8 @@ public sealed class PatronageService
|
|||||||
// if his sub would end in teh future, extend it by one month.
|
// if his sub would end in teh future, extend it by one month.
|
||||||
// if it's not, just add 1 month to the last charge date
|
// if it's not, just add 1 month to the last charge date
|
||||||
var count = await ctx.GetTable<PatronUser>()
|
var count = await ctx.GetTable<PatronUser>()
|
||||||
.Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId)
|
.Where(x => x.UniquePlatformUserId
|
||||||
|
== subscriber.UniquePlatformUserId)
|
||||||
.UpdateAsync(old => new()
|
.UpdateAsync(old => new()
|
||||||
{
|
{
|
||||||
UserId = subscriber.UserId,
|
UserId = subscriber.UserId,
|
||||||
@@ -215,14 +149,13 @@ public sealed class PatronageService
|
|||||||
: dateInOneMonth,
|
: dateInOneMonth,
|
||||||
});
|
});
|
||||||
|
|
||||||
// this should never happen
|
|
||||||
if (count == 0)
|
|
||||||
{
|
|
||||||
// await tran.RollbackAsync();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// await tran.CommitAsync();
|
dbPatron.UserId = subscriber.UserId;
|
||||||
|
dbPatron.AmountCents = subscriber.Cents;
|
||||||
|
dbPatron.LastCharge = lastChargeUtc;
|
||||||
|
dbPatron.ValidThru = dbPatron.ValidThru >= todayDate
|
||||||
|
? dbPatron.ValidThru.AddMonths(1)
|
||||||
|
: dateInOneMonth;
|
||||||
|
|
||||||
await OnNewPatronPayment(PatronUserToPatron(dbPatron));
|
await OnNewPatronPayment(PatronUserToPatron(dbPatron));
|
||||||
}
|
}
|
||||||
@@ -284,313 +217,7 @@ public sealed class PatronageService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> ExecPreCommandAsync(
|
public async Task<Patron?> GetPatronAsync(ulong userId)
|
||||||
ICommandContext ctx,
|
|
||||||
string moduleName,
|
|
||||||
CommandInfo command)
|
|
||||||
{
|
|
||||||
var ownerId = ctx.Guild?.OwnerId ?? 0;
|
|
||||||
|
|
||||||
var result = await AttemptRunCommand(
|
|
||||||
ctx.User.Id,
|
|
||||||
ownerId: ownerId,
|
|
||||||
command.Aliases.First().ToLowerInvariant(),
|
|
||||||
command.Module.Parent == null ? string.Empty : command.Module.GetGroupName().ToLowerInvariant(),
|
|
||||||
moduleName.ToLowerInvariant()
|
|
||||||
);
|
|
||||||
|
|
||||||
return result.Match(
|
|
||||||
_ => false,
|
|
||||||
ins =>
|
|
||||||
{
|
|
||||||
var eb = _sender.CreateEmbed()
|
|
||||||
.WithPendingColor()
|
|
||||||
.WithTitle("Insufficient Patron Tier")
|
|
||||||
.AddField("For", $"{ins.FeatureType}: `{ins.Feature}`", true)
|
|
||||||
.AddField("Required Tier",
|
|
||||||
$"[{ins.RequiredTier.ToFullName()}](https://patreon.com/join/nadekobot)",
|
|
||||||
true);
|
|
||||||
|
|
||||||
if (ctx.Guild is null || ctx.Guild?.OwnerId == ctx.User.Id)
|
|
||||||
eb.WithDescription("You don't have the sufficent Patron Tier to run this command.")
|
|
||||||
.WithFooter("You can use '.patron' and '.donate' commands for more info");
|
|
||||||
else
|
|
||||||
eb.WithDescription(
|
|
||||||
"Neither you nor the server owner have the sufficent Patron Tier to run this command.")
|
|
||||||
.WithFooter("You can use '.patron' and '.donate' commands for more info");
|
|
||||||
|
|
||||||
_ = ctx.WarningAsync();
|
|
||||||
|
|
||||||
if (ctx.Guild?.OwnerId == ctx.User.Id)
|
|
||||||
_ = _sender.Response(ctx)
|
|
||||||
.Context(ctx)
|
|
||||||
.Embed(eb)
|
|
||||||
.SendAsync();
|
|
||||||
else
|
|
||||||
_ = _sender.Response(ctx).User(ctx.User).Embed(eb).SendAsync();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
quota =>
|
|
||||||
{
|
|
||||||
var eb = _sender.CreateEmbed()
|
|
||||||
.WithPendingColor()
|
|
||||||
.WithTitle("Quota Limit Reached");
|
|
||||||
|
|
||||||
if (quota.IsOwnQuota || ctx.User.Id == ownerId)
|
|
||||||
{
|
|
||||||
eb.WithDescription($"You've reached your quota of `{quota.Quota} {quota.QuotaPeriod.ToFullName()}`")
|
|
||||||
.WithFooter("You may want to check your quota by using the '.patron' command.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
eb.WithDescription(
|
|
||||||
$"This server reached the quota of {quota.Quota} `{quota.QuotaPeriod.ToFullName()}`")
|
|
||||||
.WithFooter("You may contact the server owner about this issue.\n"
|
|
||||||
+ "Alternatively, you can become patron yourself by using the '.donate' command.\n"
|
|
||||||
+ "If you're already a patron, it means you've reached your quota.\n"
|
|
||||||
+ "You can use '.patron' command to check your quota status.");
|
|
||||||
}
|
|
||||||
|
|
||||||
eb.AddField("For", $"{quota.FeatureType}: `{quota.Feature}`", true)
|
|
||||||
.AddField("Resets At", quota.ResetsAt.ToShortAndRelativeTimestampTag(), true);
|
|
||||||
|
|
||||||
_ = ctx.WarningAsync();
|
|
||||||
|
|
||||||
// send the message in the server in case it's the owner
|
|
||||||
if (ctx.Guild?.OwnerId == ctx.User.Id)
|
|
||||||
_ = _sender.Response(ctx)
|
|
||||||
.Embed(eb)
|
|
||||||
.SendAsync();
|
|
||||||
else
|
|
||||||
_ = _sender.Response(ctx).User(ctx.User).Embed(eb).SendAsync();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask<OneOf<OneOf.Types.Success, InsufficientTier, QuotaLimit>> AttemptRunCommand(
|
|
||||||
ulong userId,
|
|
||||||
ulong ownerId,
|
|
||||||
string commandName,
|
|
||||||
string groupName,
|
|
||||||
string moduleName)
|
|
||||||
{
|
|
||||||
// try to run as a user
|
|
||||||
var res = await AttemptRunCommand(userId, commandName, groupName, moduleName, true);
|
|
||||||
|
|
||||||
// if it fails, try to run as an owner
|
|
||||||
// but only if the command is ran in a server
|
|
||||||
// and if the owner is not the user
|
|
||||||
if (!res.IsT0 && ownerId != 0 && ownerId != userId)
|
|
||||||
res = await AttemptRunCommand(ownerId, commandName, groupName, moduleName, false);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns either the current usage counter if limit wasn't reached, or QuotaLimit if it is.
|
|
||||||
/// </summary>
|
|
||||||
public async ValueTask<OneOf<(uint Hourly, uint Daily, uint Monthly), QuotaLimit>> TryIncrementQuotaCounterAsync(
|
|
||||||
ulong userId,
|
|
||||||
bool isSelf,
|
|
||||||
FeatureType featureType,
|
|
||||||
string featureName,
|
|
||||||
uint? maybeHourly,
|
|
||||||
uint? maybeDaily,
|
|
||||||
uint? maybeMonthly)
|
|
||||||
{
|
|
||||||
await using var ctx = _db.GetDbContext();
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
await using var tran = await ctx.Database.BeginTransactionAsync();
|
|
||||||
|
|
||||||
var userQuotaData = await ctx.GetTable<PatronQuota>()
|
|
||||||
.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId
|
|
||||||
&& x.Feature == featureName)
|
|
||||||
?? new PatronQuota();
|
|
||||||
|
|
||||||
// if hourly exists, if daily exists, etc...
|
|
||||||
if (maybeHourly is uint hourly && userQuotaData.HourlyCount >= hourly)
|
|
||||||
{
|
|
||||||
return new QuotaLimit()
|
|
||||||
{
|
|
||||||
QuotaPeriod = QuotaPer.PerHour,
|
|
||||||
Quota = hourly,
|
|
||||||
// quite a neat trick. https://stackoverflow.com/a/5733560
|
|
||||||
ResetsAt = now.Date.AddHours(now.Hour + 1),
|
|
||||||
Feature = featureName,
|
|
||||||
FeatureType = featureType,
|
|
||||||
IsOwnQuota = isSelf
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maybeDaily is uint daily
|
|
||||||
&& userQuotaData.DailyCount >= daily)
|
|
||||||
{
|
|
||||||
return new QuotaLimit()
|
|
||||||
{
|
|
||||||
QuotaPeriod = QuotaPer.PerDay,
|
|
||||||
Quota = daily,
|
|
||||||
ResetsAt = now.Date.AddDays(1),
|
|
||||||
Feature = featureName,
|
|
||||||
FeatureType = featureType,
|
|
||||||
IsOwnQuota = isSelf
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maybeMonthly is uint monthly && userQuotaData.MonthlyCount >= monthly)
|
|
||||||
{
|
|
||||||
return new QuotaLimit()
|
|
||||||
{
|
|
||||||
QuotaPeriod = QuotaPer.PerMonth,
|
|
||||||
Quota = monthly,
|
|
||||||
ResetsAt = now.Date.SecondOfNextMonth(),
|
|
||||||
Feature = featureName,
|
|
||||||
FeatureType = featureType,
|
|
||||||
IsOwnQuota = isSelf
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.GetTable<PatronQuota>()
|
|
||||||
.InsertOrUpdateAsync(() => new()
|
|
||||||
{
|
|
||||||
UserId = userId,
|
|
||||||
FeatureType = featureType,
|
|
||||||
Feature = featureName,
|
|
||||||
DailyCount = 1,
|
|
||||||
MonthlyCount = 1,
|
|
||||||
HourlyCount = 1,
|
|
||||||
},
|
|
||||||
(old) => new()
|
|
||||||
{
|
|
||||||
HourlyCount = old.HourlyCount + 1,
|
|
||||||
DailyCount = old.DailyCount + 1,
|
|
||||||
MonthlyCount = old.MonthlyCount + 1,
|
|
||||||
},
|
|
||||||
() => new()
|
|
||||||
{
|
|
||||||
UserId = userId,
|
|
||||||
FeatureType = featureType,
|
|
||||||
Feature = featureName,
|
|
||||||
});
|
|
||||||
|
|
||||||
await tran.CommitAsync();
|
|
||||||
|
|
||||||
return (userQuotaData.HourlyCount + 1, userQuotaData.DailyCount + 1, userQuotaData.MonthlyCount + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempts to add 1 to user's quota for the command, group and module.
|
|
||||||
/// Input MUST BE lowercase
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userId">Id of the user who is attempting to run the command</param>
|
|
||||||
/// <param name="commandName">Name of the command the user is trying to run</param>
|
|
||||||
/// <param name="groupName">Name of the command's group</param>
|
|
||||||
/// <param name="moduleName">Name of the command's top level module</param>
|
|
||||||
/// <param name="isSelf">Whether this is check is for the user himself. False if it's someone else's id (owner)</param>
|
|
||||||
/// <returns>Either a succcess (user can run the command) or one of the error values.</returns>
|
|
||||||
private async ValueTask<OneOf<OneOf.Types.Success, InsufficientTier, QuotaLimit>> AttemptRunCommand(
|
|
||||||
ulong userId,
|
|
||||||
string commandName,
|
|
||||||
string groupName,
|
|
||||||
string moduleName,
|
|
||||||
bool isSelf)
|
|
||||||
{
|
|
||||||
var confData = _pConf.Data;
|
|
||||||
|
|
||||||
if (!confData.IsEnabled)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
if (_creds.GetCreds().IsOwner(userId))
|
|
||||||
return default;
|
|
||||||
|
|
||||||
// get user tier
|
|
||||||
var patron = await GetPatronAsync(userId);
|
|
||||||
FeatureType quotaForFeatureType;
|
|
||||||
|
|
||||||
if (confData.Quotas.Commands.TryGetValue(commandName, out var quotaData))
|
|
||||||
{
|
|
||||||
quotaForFeatureType = FeatureType.Command;
|
|
||||||
}
|
|
||||||
else if (confData.Quotas.Groups.TryGetValue(groupName, out quotaData))
|
|
||||||
{
|
|
||||||
quotaForFeatureType = FeatureType.Group;
|
|
||||||
}
|
|
||||||
else if (confData.Quotas.Modules.TryGetValue(moduleName, out quotaData))
|
|
||||||
{
|
|
||||||
quotaForFeatureType = FeatureType.Module;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
var featureName = quotaForFeatureType switch
|
|
||||||
{
|
|
||||||
FeatureType.Command => commandName,
|
|
||||||
FeatureType.Group => groupName,
|
|
||||||
FeatureType.Module => moduleName,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(quotaForFeatureType))
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!TryGetTierDataOrLower(quotaData, patron.Tier, out var data))
|
|
||||||
{
|
|
||||||
return new InsufficientTier()
|
|
||||||
{
|
|
||||||
Feature = featureName,
|
|
||||||
FeatureType = quotaForFeatureType,
|
|
||||||
RequiredTier = quotaData.Count == 0
|
|
||||||
? PatronTier.ComingSoon
|
|
||||||
: quotaData.Keys.First(),
|
|
||||||
UserTier = patron.Tier,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// no quota limits for this tier
|
|
||||||
if (data is null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
var quotaCheckResult = await TryIncrementQuotaCounterAsync(userId,
|
|
||||||
isSelf,
|
|
||||||
quotaForFeatureType,
|
|
||||||
featureName,
|
|
||||||
data.TryGetValue(QuotaPer.PerHour, out var hourly) ? hourly : null,
|
|
||||||
data.TryGetValue(QuotaPer.PerDay, out var daily) ? daily : null,
|
|
||||||
data.TryGetValue(QuotaPer.PerMonth, out var monthly) ? monthly : null
|
|
||||||
);
|
|
||||||
|
|
||||||
return quotaCheckResult.Match<OneOf<Success, InsufficientTier, QuotaLimit>>(
|
|
||||||
_ => new Success(),
|
|
||||||
x => x);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetTierDataOrLower<T>(
|
|
||||||
IReadOnlyDictionary<PatronTier, T?> data,
|
|
||||||
PatronTier tier,
|
|
||||||
out T? o)
|
|
||||||
{
|
|
||||||
// check for quotas on this tier
|
|
||||||
if (data.TryGetValue(tier, out o))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// if there are none, get the quota first tier below this one
|
|
||||||
// which has quotas specified
|
|
||||||
for (var i = _tiers.Length - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var lowerTier = _tiers[i];
|
|
||||||
if (lowerTier < tier && data.TryGetValue(lowerTier, out o))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there are none, that means the feature is intended
|
|
||||||
// to be patron-only but the quotas haven't been specified yet
|
|
||||||
// so it will be marked as "Coming Soon"
|
|
||||||
o = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Patron> GetPatronAsync(ulong userId)
|
|
||||||
{
|
{
|
||||||
await using var ctx = _db.GetDbContext();
|
await using var ctx = _db.GetDbContext();
|
||||||
|
|
||||||
@@ -616,128 +243,135 @@ public sealed class PatronageService
|
|||||||
return PatronUserToPatron(max);
|
return PatronUserToPatron(max);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UserQuotaStats> GetUserQuotaStatistic(ulong userId)
|
public async Task<bool> LimitHitAsync(LimitedFeatureName key, ulong userId, int amount = 1)
|
||||||
{
|
{
|
||||||
var pConfData = _pConf.Data;
|
if (_creds.GetCreds().IsOwner(userId))
|
||||||
|
return true;
|
||||||
|
|
||||||
if (!pConfData.IsEnabled)
|
if (!_pConf.Data.IsEnabled)
|
||||||
return new();
|
return true;
|
||||||
|
|
||||||
var patron = await GetPatronAsync(userId);
|
var userLimit = await GetUserLimit(key, userId);
|
||||||
|
|
||||||
await using var ctx = _db.GetDbContext();
|
if (userLimit.Quota == 0)
|
||||||
var allPatronQuotas = await ctx.GetTable<PatronQuota>()
|
return false;
|
||||||
.Where(x => x.UserId == userId)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var allQuotasDict = allPatronQuotas
|
if (userLimit.Quota == -1)
|
||||||
.GroupBy(static x => x.FeatureType)
|
return true;
|
||||||
.ToDictionary(static x => x.Key, static x => x.ToDictionary(static y => y.Feature));
|
|
||||||
|
|
||||||
allQuotasDict.TryGetValue(FeatureType.Command, out var data);
|
return await TryAddLimit(key, userLimit, userId, amount);
|
||||||
var userCommandQuotaStats = GetFeatureQuotaStats(patron.Tier, data, pConfData.Quotas.Commands);
|
|
||||||
|
|
||||||
allQuotasDict.TryGetValue(FeatureType.Group, out data);
|
|
||||||
var userGroupQuotaStats = GetFeatureQuotaStats(patron.Tier, data, pConfData.Quotas.Groups);
|
|
||||||
|
|
||||||
allQuotasDict.TryGetValue(FeatureType.Module, out data);
|
|
||||||
var userModuleQuotaStats = GetFeatureQuotaStats(patron.Tier, data, pConfData.Quotas.Modules);
|
|
||||||
|
|
||||||
return new UserQuotaStats()
|
|
||||||
{
|
|
||||||
Tier = patron.Tier,
|
|
||||||
Commands = userCommandQuotaStats,
|
|
||||||
Groups = userGroupQuotaStats,
|
|
||||||
Modules = userModuleQuotaStats,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyDictionary<string, FeatureQuotaStats> GetFeatureQuotaStats(
|
public async Task<bool> LimitForceHit(LimitedFeatureName key, ulong userId, int amount)
|
||||||
PatronTier patronTier,
|
|
||||||
IReadOnlyDictionary<string, PatronQuota>? allQuotasDict,
|
|
||||||
Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> commands)
|
|
||||||
{
|
{
|
||||||
var userCommandQuotaStats = new Dictionary<string, FeatureQuotaStats>();
|
if (_creds.GetCreds().IsOwner(userId))
|
||||||
foreach (var (key, quotaData) in commands)
|
return true;
|
||||||
{
|
|
||||||
if (TryGetTierDataOrLower(quotaData, patronTier, out var data))
|
|
||||||
{
|
|
||||||
// if data is null that means the quota for the user's tier is unlimited
|
|
||||||
// no point in returning it?
|
|
||||||
|
|
||||||
if (data is null)
|
if (!_pConf.Data.IsEnabled)
|
||||||
continue;
|
return true;
|
||||||
|
|
||||||
var (daily, hourly, monthly) = default((uint, uint, uint));
|
var userLimit = await GetUserLimit(key, userId);
|
||||||
// try to get users stats for this feature
|
|
||||||
// if it fails just leave them at 0
|
|
||||||
if (allQuotasDict?.TryGetValue(key, out var quota) ?? false)
|
|
||||||
(daily, hourly, monthly) = (quota.DailyCount, quota.HourlyCount, quota.MonthlyCount);
|
|
||||||
|
|
||||||
userCommandQuotaStats[key] = new FeatureQuotaStats()
|
var cacheKey = CreateKey(key, userId);
|
||||||
|
await _cache.GetOrAddAsync(cacheKey, () => Task.FromResult(0), GetExpiry(userLimit));
|
||||||
|
|
||||||
|
return await TryAddLimit(key, userLimit, userId, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> TryAddLimit(
|
||||||
|
LimitedFeatureName key,
|
||||||
|
QuotaLimit userLimit,
|
||||||
|
ulong userId,
|
||||||
|
int amount)
|
||||||
{
|
{
|
||||||
Hourly = data.TryGetValue(QuotaPer.PerHour, out var hourD)
|
var cacheKey = CreateKey(key, userId);
|
||||||
? (hourly, hourD)
|
var cur = await _cache.GetOrAddAsync(cacheKey, () => Task.FromResult(0), GetExpiry(userLimit));
|
||||||
: default,
|
|
||||||
Daily = data.TryGetValue(QuotaPer.PerDay, out var maxD)
|
if (cur + amount < userLimit.Quota)
|
||||||
? (daily, maxD)
|
{
|
||||||
: default,
|
await _cache.AddAsync(cacheKey, cur + amount);
|
||||||
Monthly = data.TryGetValue(QuotaPer.PerMonth, out var maxM)
|
return true;
|
||||||
? (monthly, maxM)
|
}
|
||||||
: default,
|
|
||||||
};
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TimeSpan? GetExpiry(QuotaLimit userLimit)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
switch (userLimit.QuotaPeriod)
|
||||||
|
{
|
||||||
|
case QuotaPer.PerHour:
|
||||||
|
return TimeSpan.FromMinutes(60 - now.Minute);
|
||||||
|
case QuotaPer.PerDay:
|
||||||
|
return TimeSpan.FromMinutes((24 * 60) - ((now.Hour * 60) + now.Minute));
|
||||||
|
case QuotaPer.PerMonth:
|
||||||
|
var firstOfNextMonth = now.FirstOfNextMonth();
|
||||||
|
return firstOfNextMonth - now;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return userCommandQuotaStats;
|
private TypedKey<int> CreateKey(LimitedFeatureName key, ulong userId)
|
||||||
}
|
=> new($"limited_feature:{key}:{userId}");
|
||||||
|
|
||||||
public async Task<FeatureLimit> TryGetFeatureLimitAsync(FeatureLimitKey key, ulong userId, int? defaultValue)
|
private readonly QuotaLimit _emptyQuota = new QuotaLimit()
|
||||||
{
|
{
|
||||||
var conf = _pConf.Data;
|
|
||||||
|
|
||||||
// if patron system is disabled, the quota is just default
|
|
||||||
if (!conf.IsEnabled)
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
Name = key.PrettyName,
|
|
||||||
Quota = defaultValue,
|
|
||||||
IsPatronLimit = false
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
if (!conf.Quotas.Features.TryGetValue(key.Key, out var data))
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
Name = key.PrettyName,
|
|
||||||
Quota = defaultValue,
|
|
||||||
IsPatronLimit = false,
|
|
||||||
};
|
|
||||||
|
|
||||||
var patron = await GetPatronAsync(userId);
|
|
||||||
if (!TryGetTierDataOrLower(data, patron.Tier, out var limit))
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
Name = key.PrettyName,
|
|
||||||
Quota = 0,
|
Quota = 0,
|
||||||
IsPatronLimit = true,
|
QuotaPeriod = QuotaPer.PerDay,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new()
|
private readonly QuotaLimit _infiniteQuota = new QuotaLimit()
|
||||||
{
|
{
|
||||||
Name = key.PrettyName,
|
Quota = -1,
|
||||||
Quota = limit,
|
QuotaPeriod = QuotaPer.PerDay,
|
||||||
IsPatronLimit = true
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public async Task<QuotaLimit> GetUserLimit(LimitedFeatureName name, ulong userId)
|
||||||
|
{
|
||||||
|
if (!_pConf.Data.IsEnabled)
|
||||||
|
return _infiniteQuota;
|
||||||
|
|
||||||
|
var maybePatron = await GetPatronAsync(userId);
|
||||||
|
|
||||||
|
if (maybePatron is not { } patron)
|
||||||
|
return _emptyQuota;
|
||||||
|
|
||||||
|
if (patron.ValidThru < DateTime.UtcNow)
|
||||||
|
return _emptyQuota;
|
||||||
|
|
||||||
|
foreach (var (key, value) in _pConf.Data.Limits)
|
||||||
|
{
|
||||||
|
if (patron.Amount >= key)
|
||||||
|
{
|
||||||
|
if (value.TryGetValue(name, out var quotaLimit))
|
||||||
|
{
|
||||||
|
return quotaLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _emptyQuota;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<LimitedFeatureName, (int, QuotaLimit)>> LimitStats(ulong userId)
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<LimitedFeatureName, (int, QuotaLimit)>();
|
||||||
|
foreach (var featureName in Enum.GetValues<LimitedFeatureName>())
|
||||||
|
{
|
||||||
|
var cacheKey = CreateKey(featureName, userId);
|
||||||
|
var userLimit = await GetUserLimit(featureName, userId);
|
||||||
|
var cur = await _cache.GetOrAddAsync(cacheKey, () => Task.FromResult(0), GetExpiry(userLimit));
|
||||||
|
|
||||||
|
dict[featureName] = (cur, userLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
// public async Task<Patron> GiftPatronAsync(IUser user, int amount)
|
|
||||||
// {
|
|
||||||
// if (amount < 1)
|
|
||||||
// throw new ArgumentOutOfRangeException(nameof(amount));
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
|
|
||||||
private Patron PatronUserToPatron(PatronUser user)
|
private Patron PatronUserToPatron(PatronUser user)
|
||||||
=> new Patron()
|
=> new Patron()
|
||||||
@@ -767,6 +401,22 @@ public sealed class PatronageService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int PercentBonus(Patron? maybePatron)
|
||||||
|
=> maybePatron is { } user && user.ValidThru > DateTime.UtcNow
|
||||||
|
? PercentBonus(user.Amount)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
public int PercentBonus(long amount)
|
||||||
|
=> amount switch
|
||||||
|
{
|
||||||
|
>= 10_000 => 100,
|
||||||
|
>= 5000 => 50,
|
||||||
|
>= 2000 => 20,
|
||||||
|
>= 1000 => 10,
|
||||||
|
>= 500 => 5,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
|
||||||
private async Task SendWelcomeMessage(Patron patron)
|
private async Task SendWelcomeMessage(Patron patron)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@@ -105,10 +105,7 @@ public partial class Searches
|
|||||||
await message.ModifyAsync(mp =>
|
await message.ModifyAsync(mp =>
|
||||||
{
|
{
|
||||||
mp.Attachments =
|
mp.Attachments =
|
||||||
new(new[]
|
new(new[] { attachment });
|
||||||
{
|
|
||||||
attachment
|
|
||||||
});
|
|
||||||
|
|
||||||
mp.Embed = eb.WithImageUrl($"attachment://{fileName}").Build();
|
mp.Embed = eb.WithImageUrl($"attachment://{fileName}").Build();
|
||||||
});
|
});
|
||||||
@@ -165,7 +162,8 @@ public partial class Searches
|
|||||||
.WithAuthor($"#{crypto.CmcRank}")
|
.WithAuthor($"#{crypto.CmcRank}")
|
||||||
.WithTitle($"{crypto.Name} ({crypto.Symbol})")
|
.WithTitle($"{crypto.Name} ({crypto.Symbol})")
|
||||||
.WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/")
|
.WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/")
|
||||||
.WithThumbnailUrl($"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
|
.WithThumbnailUrl(
|
||||||
|
$"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
|
||||||
.AddField(GetText(strs.market_cap), marketCap, true)
|
.AddField(GetText(strs.market_cap), marketCap, true)
|
||||||
.AddField(GetText(strs.price), price, true)
|
.AddField(GetText(strs.price), price, true)
|
||||||
.AddField(GetText(strs.volume_24h), volume, true)
|
.AddField(GetText(strs.volume_24h), volume, true)
|
||||||
@@ -192,5 +190,54 @@ public partial class Searches
|
|||||||
|
|
||||||
await ctx.Channel.SendFileAsync(sparkline, fileName, embed: toSend.Build());
|
await ctx.Channel.SendFileAsync(sparkline, fileName, embed: toSend.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
public async Task Coins(int page = 1)
|
||||||
|
{
|
||||||
|
if (--page < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (page > 25)
|
||||||
|
page = 25;
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Paginated()
|
||||||
|
.PageItems(async (page) =>
|
||||||
|
{
|
||||||
|
var coins = await _service.GetTopCoins(page);
|
||||||
|
return coins;
|
||||||
|
})
|
||||||
|
.PageSize(10)
|
||||||
|
.Page((items, _) =>
|
||||||
|
{
|
||||||
|
var embed = _sender.CreateEmbed()
|
||||||
|
.WithOkColor();
|
||||||
|
|
||||||
|
if (items.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var coin in items)
|
||||||
|
{
|
||||||
|
embed.AddField($"#{coin.MarketCapRank} {coin.Symbol} - {coin.Name}",
|
||||||
|
$"""
|
||||||
|
`Price:` {GetArrowEmoji(coin.PercentChange24h)} {coin.CurrentPrice.ToShortString()}$ ({GetSign(coin.PercentChange24h)}{Math.Round(coin.PercentChange24h, 2)}%)
|
||||||
|
`MarketCap:` {coin.MarketCap.ToShortString()}$
|
||||||
|
`Supply:` {(coin.CirculatingSupply?.ToShortString() ?? "?")} / {(coin.TotalSupply?.ToShortString() ?? "?")}
|
||||||
|
""",
|
||||||
|
inline: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return embed;
|
||||||
|
})
|
||||||
|
.CurrentPage(page)
|
||||||
|
.AddFooter(false)
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetArrowEmoji(decimal value)
|
||||||
|
=> value > 0 ? "▲" : "▼";
|
||||||
|
|
||||||
|
private static string GetSign(decimal value)
|
||||||
|
=> value >= 0 ? "+" : "-";
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -4,8 +4,10 @@ using SixLabors.ImageSharp;
|
|||||||
using SixLabors.ImageSharp.Drawing.Processing;
|
using SixLabors.ImageSharp.Drawing.Processing;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using Color = SixLabors.ImageSharp.Color;
|
using Color = SixLabors.ImageSharp.Color;
|
||||||
using StringExtensions = NadekoBot.Extensions.StringExtensions;
|
using StringExtensions = NadekoBot.Extensions.StringExtensions;
|
||||||
@@ -212,4 +214,55 @@ public class CryptoService : INService
|
|||||||
var points = GetSparklinePointsFromSvgText(str);
|
var points = GetSparklinePointsFromSvgText(str);
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static TypedKey<IReadOnlyCollection<GeckoCoinsResult>> GetTopCoinsKey()
|
||||||
|
=> new($"crypto:top_coins");
|
||||||
|
|
||||||
|
public async Task<IReadOnlyCollection<GeckoCoinsResult>?> GetTopCoins(int page)
|
||||||
|
{
|
||||||
|
if (page >= 25)
|
||||||
|
page = 24;
|
||||||
|
|
||||||
|
using var http = _httpFactory.CreateClient();
|
||||||
|
|
||||||
|
http.AddFakeHeaders();
|
||||||
|
|
||||||
|
var result = await _cache.GetOrAddAsync<IReadOnlyCollection<GeckoCoinsResult>>(GetTopCoinsKey(),
|
||||||
|
async () => await http.GetFromJsonAsync<List<GeckoCoinsResult>>(
|
||||||
|
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=250")
|
||||||
|
?? [],
|
||||||
|
expiry: TimeSpan.FromHours(1));
|
||||||
|
|
||||||
|
return result!.Skip(page * 10).Take(10).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GeckoCoinsResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public required string Id { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public required string Name { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("symbol")]
|
||||||
|
public required string Symbol { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("current_price")]
|
||||||
|
public required decimal CurrentPrice { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("price_change_percentage_24h")]
|
||||||
|
public required decimal PercentChange24h { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("market_cap")]
|
||||||
|
public required decimal MarketCap { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("circulating_supply")]
|
||||||
|
public required decimal? CirculatingSupply { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("total_supply")]
|
||||||
|
public required decimal? TotalSupply { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("market_cap_rank")]
|
||||||
|
public required int MarketCapRank { get; init; }
|
||||||
}
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
using AngleSharp;
|
using AngleSharp;
|
||||||
using CsvHelper;
|
using CsvHelper;
|
||||||
using CsvHelper.Configuration;
|
using CsvHelper.Configuration;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
@@ -22,46 +23,32 @@ public sealed class DefaultStockDataService : IStockDataService, INService
|
|||||||
|
|
||||||
using var http = _httpClientFactory.CreateClient();
|
using var http = _httpClientFactory.CreateClient();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var quoteHtmlPage = $"https://finance.yahoo.com/quote/{query.ToUpperInvariant()}";
|
var quoteHtmlPage = $"https://finance.yahoo.com/quote/{query.ToUpperInvariant()}";
|
||||||
|
|
||||||
var config = Configuration.Default.WithDefaultLoader();
|
var config = Configuration.Default.WithDefaultLoader();
|
||||||
using var document = await BrowsingContext.New(config).OpenAsync(quoteHtmlPage);
|
using var document = await BrowsingContext.New(config).OpenAsync(quoteHtmlPage);
|
||||||
var divElem =
|
|
||||||
document.QuerySelector(
|
var tickerName = document.QuerySelector("div.top > .left > .container > h1")
|
||||||
"#quote-header-info > div:nth-child(2) > div > div > h1");
|
?.TextContent;
|
||||||
var tickerName = (divElem)?.TextContent;
|
|
||||||
|
if (tickerName is null)
|
||||||
|
return default;
|
||||||
|
|
||||||
var marketcap = document
|
var marketcap = document
|
||||||
.QuerySelectorAll("table")
|
.QuerySelector("li > span > fin-streamer[data-field='marketCap']")
|
||||||
.Skip(1)
|
|
||||||
.First()
|
|
||||||
.QuerySelector("tbody > tr > td:nth-child(2)")
|
|
||||||
?.TextContent;
|
?.TextContent;
|
||||||
|
|
||||||
|
|
||||||
var volume = document.QuerySelector("td[data-test='AVERAGE_VOLUME_3MONTH-value']")
|
var volume = document.QuerySelector("li > span > fin-streamer[data-field='regularMarketVolume']")
|
||||||
?.TextContent;
|
?.TextContent;
|
||||||
|
|
||||||
var close= document.QuerySelector("td[data-test='PREV_CLOSE-value']")
|
var close = document.QuerySelector("li > span > fin-streamer[data-field='regularMarketPreviousClose']")
|
||||||
?.TextContent ?? "0";
|
?.TextContent
|
||||||
|
?? "0";
|
||||||
|
|
||||||
var price = document
|
var price = document.QuerySelector("fin-streamer.livePrice > span")
|
||||||
.QuerySelector("#quote-header-info")
|
?.TextContent
|
||||||
?.QuerySelector("fin-streamer[data-field='regularMarketPrice']")
|
?? "0";
|
||||||
?.TextContent ?? close;
|
|
||||||
|
|
||||||
// var data = await http.GetFromJsonAsync<YahooQueryModel>(
|
|
||||||
// $"https://query1.finance.yahoo.com/v7/finance/quote?symbols={query}");
|
|
||||||
//
|
|
||||||
// if (data is null)
|
|
||||||
// return default;
|
|
||||||
|
|
||||||
// var symbol = data.QuoteResponse.Result.FirstOrDefault();
|
|
||||||
|
|
||||||
// if (symbol is null)
|
|
||||||
// return default;
|
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
#nullable disable
|
using CodeHollow.FeedReader;
|
||||||
using CodeHollow.FeedReader;
|
|
||||||
using NadekoBot.Modules.Searches.Services;
|
using NadekoBot.Modules.Searches.Services;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@@ -17,19 +16,21 @@ public partial class Searches
|
|||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[UserPerm(GuildPerm.ManageMessages)]
|
[UserPerm(GuildPerm.ManageMessages)]
|
||||||
[Priority(1)]
|
[Priority(1)]
|
||||||
public Task YtUploadNotif(string url, [Leftover] string message = null)
|
public Task YtUploadNotif(string url, [Leftover] string? message = null)
|
||||||
=> YtUploadNotif(url, null, message);
|
=> YtUploadNotif(url, null, message);
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[UserPerm(GuildPerm.ManageMessages)]
|
[UserPerm(GuildPerm.ManageMessages)]
|
||||||
[Priority(2)]
|
[Priority(2)]
|
||||||
public Task YtUploadNotif(string url, ITextChannel channel = null, [Leftover] string message = null)
|
public Task YtUploadNotif(string url, ITextChannel? channel = null, [Leftover] string? message = null)
|
||||||
{
|
{
|
||||||
var m = _ytChannelRegex.Match(url);
|
var m = _ytChannelRegex.Match(url);
|
||||||
if (!m.Success)
|
if (!m.Success)
|
||||||
return Response().Error(strs.invalid_input).SendAsync();
|
return Response().Error(strs.invalid_input).SendAsync();
|
||||||
|
|
||||||
|
channel ??= ctx.Channel as ITextChannel;
|
||||||
|
|
||||||
if (!((IGuildUser)ctx.User).GetPermissions(channel).MentionEveryone)
|
if (!((IGuildUser)ctx.User).GetPermissions(channel).MentionEveryone)
|
||||||
message = message?.SanitizeAllMentions();
|
message = message?.SanitizeAllMentions();
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ public partial class Searches
|
|||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[UserPerm(GuildPerm.ManageMessages)]
|
[UserPerm(GuildPerm.ManageMessages)]
|
||||||
[Priority(0)]
|
[Priority(0)]
|
||||||
public Task Feed(string url, [Leftover] string message = null)
|
public Task Feed(string url, [Leftover] string? message = null)
|
||||||
=> Feed(url, null, message);
|
=> Feed(url, null, message);
|
||||||
|
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ public partial class Searches
|
|||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
[UserPerm(GuildPerm.ManageMessages)]
|
[UserPerm(GuildPerm.ManageMessages)]
|
||||||
[Priority(1)]
|
[Priority(1)]
|
||||||
public async Task Feed(string url, ITextChannel channel = null, [Leftover] string message = null)
|
public async Task Feed(string url, ITextChannel? channel = null, [Leftover] string? message = null)
|
||||||
{
|
{
|
||||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)
|
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)
|
||||||
|| (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
|
|| (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
|
||||||
@@ -59,10 +60,11 @@ public partial class Searches
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channel ??= (ITextChannel)ctx.Channel;
|
||||||
|
|
||||||
if (!((IGuildUser)ctx.User).GetPermissions(channel).MentionEveryone)
|
if (!((IGuildUser)ctx.User).GetPermissions(channel).MentionEveryone)
|
||||||
message = message?.SanitizeAllMentions();
|
message = message?.SanitizeAllMentions();
|
||||||
|
|
||||||
channel ??= (ITextChannel)ctx.Channel;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await FeedReader.ReadAsync(url);
|
await FeedReader.ReadAsync(url);
|
||||||
|
@@ -79,6 +79,15 @@ public class FeedsService : INService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DateTime? GetPubDate(FeedItem item)
|
||||||
|
{
|
||||||
|
if (item.PublishingDate is not null)
|
||||||
|
return item.PublishingDate;
|
||||||
|
if (item.SpecificItem is AtomFeedItem atomItem)
|
||||||
|
return atomItem.UpdatedDate;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<EmbedBuilder> TrackFeeds()
|
public async Task<EmbedBuilder> TrackFeeds()
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
@@ -94,24 +103,32 @@ public class FeedsService : INService
|
|||||||
{
|
{
|
||||||
var feed = await FeedReader.ReadAsync(rssUrl);
|
var feed = await FeedReader.ReadAsync(rssUrl);
|
||||||
|
|
||||||
var items = feed
|
var items = new List<(FeedItem Item, DateTime LastUpdate)>();
|
||||||
.Items.Select(item => (Item: item,
|
foreach (var item in feed.Items)
|
||||||
LastUpdate: item.PublishingDate?.ToUniversalTime()
|
{
|
||||||
?? (item.SpecificItem as AtomFeedItem)?.UpdatedDate?.ToUniversalTime()))
|
var pubDate = GetPubDate(item);
|
||||||
.Where(data => data.LastUpdate is not null)
|
|
||||||
.Select(data => (data.Item, LastUpdate: (DateTime)data.LastUpdate))
|
if (pubDate is null)
|
||||||
.OrderByDescending(data => data.LastUpdate)
|
continue;
|
||||||
.Reverse() // start from the oldest
|
|
||||||
.ToList();
|
items.Add((item, pubDate.Value.ToUniversalTime()));
|
||||||
|
|
||||||
|
// show at most 3 items if you're behind
|
||||||
|
if (items.Count > 2)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
if (!_lastPosts.TryGetValue(kvp.Key, out var lastFeedUpdate))
|
if (!_lastPosts.TryGetValue(kvp.Key, out var lastFeedUpdate))
|
||||||
{
|
{
|
||||||
lastFeedUpdate = _lastPosts[kvp.Key] =
|
lastFeedUpdate = _lastPosts[kvp.Key] = items[0].LastUpdate;
|
||||||
items.Any() ? items[items.Count - 1].LastUpdate : DateTime.UtcNow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (feedItem, itemUpdateDate) in items)
|
for (var index = 1; index <= items.Count; index++)
|
||||||
{
|
{
|
||||||
|
var (feedItem, itemUpdateDate) = items[^index];
|
||||||
if (itemUpdateDate <= lastFeedUpdate)
|
if (itemUpdateDate <= lastFeedUpdate)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -168,27 +185,26 @@ public class FeedsService : INService
|
|||||||
if (!string.IsNullOrWhiteSpace(feedItem.Description))
|
if (!string.IsNullOrWhiteSpace(feedItem.Description))
|
||||||
embed.WithDescription(desc.TrimTo(2048));
|
embed.WithDescription(desc.TrimTo(2048));
|
||||||
|
|
||||||
//send the created embed to all subscribed channels
|
|
||||||
var feedSendTasks = kvp.Value
|
var tasks = new List<Task>();
|
||||||
.Where(x => x.GuildConfig is not null)
|
|
||||||
.Select(x =>
|
foreach (var val in kvp.Value)
|
||||||
{
|
{
|
||||||
var ch = _client.GetGuild(x.GuildConfig.GuildId)
|
var ch = _client.GetGuild(val.GuildConfig.GuildId).GetTextChannel(val.ChannelId);
|
||||||
?.GetTextChannel(x.ChannelId);
|
|
||||||
|
|
||||||
if (ch is null)
|
if (ch is null)
|
||||||
return null;
|
continue;
|
||||||
|
|
||||||
return _sender.Response(ch)
|
var sendTask = _sender.Response(ch)
|
||||||
.Embed(embed)
|
.Embed(embed)
|
||||||
.Text(string.IsNullOrWhiteSpace(x.Message)
|
.Text(string.IsNullOrWhiteSpace(val.Message)
|
||||||
? string.Empty
|
? string.Empty
|
||||||
: x.Message)
|
: val.Message)
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
})
|
tasks.Add(sendTask);
|
||||||
.Where(x => x is not null);
|
}
|
||||||
|
|
||||||
allSendTasks.Add(feedSendTasks.WhenAll());
|
allSendTasks.Add(tasks.WhenAll());
|
||||||
|
|
||||||
// as data retrieval was successful, reset error counter
|
// as data retrieval was successful, reset error counter
|
||||||
ClearErrors(rssUrl);
|
ClearErrors(rssUrl);
|
||||||
|
@@ -1,312 +0,0 @@
|
|||||||
#nullable disable
|
|
||||||
using NadekoBot.Modules.Searches.Common;
|
|
||||||
using NadekoBot.Modules.Searches.Services;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Searches;
|
|
||||||
|
|
||||||
public partial class Searches
|
|
||||||
{
|
|
||||||
[Group]
|
|
||||||
public partial class PathOfExileCommands : NadekoModule<SearchesService>
|
|
||||||
{
|
|
||||||
private const string POE_URL = "https://www.pathofexile.com/character-window/get-characters?accountName=";
|
|
||||||
private const string PON_URL = "http://poe.ninja/api/Data/GetCurrencyOverview?league=";
|
|
||||||
private const string POGS_URL = "http://pathofexile.gamepedia.com/api.php?action=opensearch&search=";
|
|
||||||
|
|
||||||
private const string POG_URL =
|
|
||||||
"https://pathofexile.gamepedia.com/api.php?action=browsebysubject&format=json&subject=";
|
|
||||||
|
|
||||||
private const string POGI_URL =
|
|
||||||
"https://pathofexile.gamepedia.com/api.php?action=query&prop=imageinfo&iiprop=url&format=json&titles=File:";
|
|
||||||
|
|
||||||
private const string PROFILE_URL = "https://www.pathofexile.com/account/view-profile/";
|
|
||||||
|
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
|
||||||
|
|
||||||
private Dictionary<string, string> currencyDictionary = new(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
{ "Chaos Orb", "Chaos Orb" },
|
|
||||||
{ "Orb of Alchemy", "Orb of Alchemy" },
|
|
||||||
{ "Jeweller's Orb", "Jeweller's Orb" },
|
|
||||||
{ "Exalted Orb", "Exalted Orb" },
|
|
||||||
{ "Mirror of Kalandra", "Mirror of Kalandra" },
|
|
||||||
{ "Vaal Orb", "Vaal Orb" },
|
|
||||||
{ "Orb of Alteration", "Orb of Alteration" },
|
|
||||||
{ "Orb of Scouring", "Orb of Scouring" },
|
|
||||||
{ "Divine Orb", "Divine Orb" },
|
|
||||||
{ "Orb of Annulment", "Orb of Annulment" },
|
|
||||||
{ "Master Cartographer's Sextant", "Master Cartographer's Sextant" },
|
|
||||||
{ "Journeyman Cartographer's Sextant", "Journeyman Cartographer's Sextant" },
|
|
||||||
{ "Apprentice Cartographer's Sextant", "Apprentice Cartographer's Sextant" },
|
|
||||||
{ "Blessed Orb", "Blessed Orb" },
|
|
||||||
{ "Orb of Regret", "Orb of Regret" },
|
|
||||||
{ "Gemcutter's Prism", "Gemcutter's Prism" },
|
|
||||||
{ "Glassblower's Bauble", "Glassblower's Bauble" },
|
|
||||||
{ "Orb of Fusing", "Orb of Fusing" },
|
|
||||||
{ "Cartographer's Chisel", "Cartographer's Chisel" },
|
|
||||||
{ "Chromatic Orb", "Chromatic Orb" },
|
|
||||||
{ "Orb of Augmentation", "Orb of Augmentation" },
|
|
||||||
{ "Blacksmith's Whetstone", "Blacksmith's Whetstone" },
|
|
||||||
{ "Orb of Transmutation", "Orb of Transmutation" },
|
|
||||||
{ "Armourer's Scrap", "Armourer's Scrap" },
|
|
||||||
{ "Scroll of Wisdom", "Scroll of Wisdom" },
|
|
||||||
{ "Regal Orb", "Regal Orb" },
|
|
||||||
{ "Chaos", "Chaos Orb" },
|
|
||||||
{ "Alch", "Orb of Alchemy" },
|
|
||||||
{ "Alchs", "Orb of Alchemy" },
|
|
||||||
{ "Jews", "Jeweller's Orb" },
|
|
||||||
{ "Jeweller", "Jeweller's Orb" },
|
|
||||||
{ "Jewellers", "Jeweller's Orb" },
|
|
||||||
{ "Jeweller's", "Jeweller's Orb" },
|
|
||||||
{ "X", "Exalted Orb" },
|
|
||||||
{ "Ex", "Exalted Orb" },
|
|
||||||
{ "Exalt", "Exalted Orb" },
|
|
||||||
{ "Exalts", "Exalted Orb" },
|
|
||||||
{ "Mirror", "Mirror of Kalandra" },
|
|
||||||
{ "Mirrors", "Mirror of Kalandra" },
|
|
||||||
{ "Vaal", "Vaal Orb" },
|
|
||||||
{ "Alt", "Orb of Alteration" },
|
|
||||||
{ "Alts", "Orb of Alteration" },
|
|
||||||
{ "Scour", "Orb of Scouring" },
|
|
||||||
{ "Scours", "Orb of Scouring" },
|
|
||||||
{ "Divine", "Divine Orb" },
|
|
||||||
{ "Annul", "Orb of Annulment" },
|
|
||||||
{ "Annulment", "Orb of Annulment" },
|
|
||||||
{ "Master Sextant", "Master Cartographer's Sextant" },
|
|
||||||
{ "Journeyman Sextant", "Journeyman Cartographer's Sextant" },
|
|
||||||
{ "Apprentice Sextant", "Apprentice Cartographer's Sextant" },
|
|
||||||
{ "Blessed", "Blessed Orb" },
|
|
||||||
{ "Regret", "Orb of Regret" },
|
|
||||||
{ "Regrets", "Orb of Regret" },
|
|
||||||
{ "Gcp", "Gemcutter's Prism" },
|
|
||||||
{ "Glassblowers", "Glassblower's Bauble" },
|
|
||||||
{ "Glassblower's", "Glassblower's Bauble" },
|
|
||||||
{ "Fusing", "Orb of Fusing" },
|
|
||||||
{ "Fuses", "Orb of Fusing" },
|
|
||||||
{ "Fuse", "Orb of Fusing" },
|
|
||||||
{ "Chisel", "Cartographer's Chisel" },
|
|
||||||
{ "Chisels", "Cartographer's Chisel" },
|
|
||||||
{ "Chance", "Orb of Chance" },
|
|
||||||
{ "Chances", "Orb of Chance" },
|
|
||||||
{ "Chrome", "Chromatic Orb" },
|
|
||||||
{ "Chromes", "Chromatic Orb" },
|
|
||||||
{ "Aug", "Orb of Augmentation" },
|
|
||||||
{ "Augmentation", "Orb of Augmentation" },
|
|
||||||
{ "Augment", "Orb of Augmentation" },
|
|
||||||
{ "Augments", "Orb of Augmentation" },
|
|
||||||
{ "Whetstone", "Blacksmith's Whetstone" },
|
|
||||||
{ "Whetstones", "Blacksmith's Whetstone" },
|
|
||||||
{ "Transmute", "Orb of Transmutation" },
|
|
||||||
{ "Transmutes", "Orb of Transmutation" },
|
|
||||||
{ "Armourers", "Armourer's Scrap" },
|
|
||||||
{ "Armourer's", "Armourer's Scrap" },
|
|
||||||
{ "Wisdom Scroll", "Scroll of Wisdom" },
|
|
||||||
{ "Wisdom Scrolls", "Scroll of Wisdom" },
|
|
||||||
{ "Regal", "Regal Orb" },
|
|
||||||
{ "Regals", "Regal Orb" }
|
|
||||||
};
|
|
||||||
|
|
||||||
public PathOfExileCommands(IHttpClientFactory httpFactory)
|
|
||||||
=> _httpFactory = httpFactory;
|
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
public async Task PathOfExile(string usr, string league = "", int page = 1)
|
|
||||||
{
|
|
||||||
if (--page < 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(usr))
|
|
||||||
{
|
|
||||||
await Response().Error("Please provide an account name.").SendAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var characters = new List<Account>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var http = _httpFactory.CreateClient();
|
|
||||||
var res = await http.GetStringAsync($"{POE_URL}{usr}");
|
|
||||||
characters = JsonConvert.DeserializeObject<List<Account>>(res);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
var embed = _sender.CreateEmbed().WithDescription(GetText(strs.account_not_found)).WithErrorColor();
|
|
||||||
|
|
||||||
await Response().Embed(embed).SendAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(league))
|
|
||||||
characters.RemoveAll(c => c.League != league);
|
|
||||||
|
|
||||||
await Response()
|
|
||||||
.Paginated()
|
|
||||||
.Items(characters)
|
|
||||||
.PageSize(9)
|
|
||||||
.CurrentPage(page)
|
|
||||||
.Page((items, curPage) =>
|
|
||||||
{
|
|
||||||
var embed = _sender.CreateEmbed()
|
|
||||||
.WithAuthor($"Characters on {usr}'s account",
|
|
||||||
"https://web.poecdn.com/image/favicon/ogimage.png",
|
|
||||||
$"{PROFILE_URL}{usr}")
|
|
||||||
.WithOkColor();
|
|
||||||
|
|
||||||
if (characters.Count == 0)
|
|
||||||
return embed.WithDescription("This account has no characters.");
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine($"```{"#",-5}{"Character Name",-23}{"League",-10}{"Class",-13}{"Level",-3}");
|
|
||||||
for (var i = 0; i < items.Count; i++)
|
|
||||||
{
|
|
||||||
var character = items[i];
|
|
||||||
|
|
||||||
sb.AppendLine(
|
|
||||||
$"#{i + 1 + (curPage * 9),-4}{character.Name,-23}{ShortLeagueName(character.League),-10}{character.Class,-13}{character.Level,-3}");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine("```");
|
|
||||||
embed.WithDescription(sb.ToString());
|
|
||||||
|
|
||||||
return embed;
|
|
||||||
})
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
public async Task PathOfExileLeagues()
|
|
||||||
{
|
|
||||||
var leagues = new List<Leagues>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var http = _httpFactory.CreateClient();
|
|
||||||
var res = await http.GetStringAsync("http://api.pathofexile.com/leagues?type=main&compact=1");
|
|
||||||
leagues = JsonConvert.DeserializeObject<List<Leagues>>(res);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
var eembed = _sender.CreateEmbed().WithDescription(GetText(strs.leagues_not_found)).WithErrorColor();
|
|
||||||
|
|
||||||
await Response().Embed(eembed).SendAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var embed = _sender.CreateEmbed()
|
|
||||||
.WithAuthor("Path of Exile Leagues",
|
|
||||||
"https://web.poecdn.com/image/favicon/ogimage.png",
|
|
||||||
"https://www.pathofexile.com")
|
|
||||||
.WithOkColor();
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine($"```{"#",-5}{"League Name",-23}");
|
|
||||||
for (var i = 0; i < leagues.Count; i++)
|
|
||||||
{
|
|
||||||
var league = leagues[i];
|
|
||||||
|
|
||||||
sb.AppendLine($"#{i + 1,-4}{league.Id,-23}");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine("```");
|
|
||||||
|
|
||||||
embed.WithDescription(sb.ToString());
|
|
||||||
|
|
||||||
await Response().Embed(embed).SendAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
public async Task PathOfExileCurrency(
|
|
||||||
string leagueName,
|
|
||||||
string currencyName,
|
|
||||||
string convertName = "Chaos Orb")
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(leagueName))
|
|
||||||
{
|
|
||||||
await Response().Error("Please provide league name.").SendAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(currencyName))
|
|
||||||
{
|
|
||||||
await Response().Error("Please provide currency name.").SendAsync();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cleanCurrency = ShortCurrencyName(currencyName);
|
|
||||||
var cleanConvert = ShortCurrencyName(convertName);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var res = $"{PON_URL}{leagueName}";
|
|
||||||
using var http = _httpFactory.CreateClient();
|
|
||||||
var obj = JObject.Parse(await http.GetStringAsync(res));
|
|
||||||
|
|
||||||
var chaosEquivalent = 0.0F;
|
|
||||||
var conversionEquivalent = 0.0F;
|
|
||||||
|
|
||||||
// poe.ninja API does not include a "chaosEquivalent" property for Chaos Orbs.
|
|
||||||
if (cleanCurrency == "Chaos Orb")
|
|
||||||
chaosEquivalent = 1.0F;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var currencyInput = obj["lines"]
|
|
||||||
.Values<JObject>()
|
|
||||||
.Where(i => i["currencyTypeName"].Value<string>() == cleanCurrency)
|
|
||||||
.FirstOrDefault();
|
|
||||||
chaosEquivalent = float.Parse(currencyInput["chaosEquivalent"].ToString(),
|
|
||||||
CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanConvert == "Chaos Orb")
|
|
||||||
conversionEquivalent = 1.0F;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var currencyOutput = obj["lines"]
|
|
||||||
.Values<JObject>()
|
|
||||||
.Where(i => i["currencyTypeName"].Value<string>() == cleanConvert)
|
|
||||||
.FirstOrDefault();
|
|
||||||
conversionEquivalent = float.Parse(currencyOutput["chaosEquivalent"].ToString(),
|
|
||||||
CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
var embed = _sender.CreateEmbed()
|
|
||||||
.WithAuthor($"{leagueName} Currency Exchange",
|
|
||||||
"https://web.poecdn.com/image/favicon/ogimage.png",
|
|
||||||
"http://poe.ninja")
|
|
||||||
.AddField("Currency Type", cleanCurrency, true)
|
|
||||||
.AddField($"{cleanConvert} Equivalent", chaosEquivalent / conversionEquivalent, true)
|
|
||||||
.WithOkColor();
|
|
||||||
|
|
||||||
await Response().Embed(embed).SendAsync();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
var embed = _sender.CreateEmbed().WithDescription(GetText(strs.ninja_not_found)).WithErrorColor();
|
|
||||||
|
|
||||||
await Response().Embed(embed).SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ShortCurrencyName(string str)
|
|
||||||
{
|
|
||||||
if (currencyDictionary.ContainsValue(str))
|
|
||||||
return str;
|
|
||||||
|
|
||||||
var currency = currencyDictionary[str];
|
|
||||||
|
|
||||||
return currency;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ShortLeagueName(string str)
|
|
||||||
{
|
|
||||||
var league = str.Replace("Hardcore", "HC", StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
|
|
||||||
return league;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
102
src/NadekoBot/Modules/Searches/ReligiousCommands.cs
Normal file
102
src/NadekoBot/Modules/Searches/ReligiousCommands.cs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
using NadekoBot.Modules.Searches.Common;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Searches;
|
||||||
|
|
||||||
|
public partial class Searches
|
||||||
|
{
|
||||||
|
public partial class ReligiousCommands : NadekoModule
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
|
|
||||||
|
public ReligiousCommands(IHttpClientFactory httpFactory)
|
||||||
|
{
|
||||||
|
_httpFactory = httpFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
public async Task Bible(string book, string chapterAndVerse)
|
||||||
|
{
|
||||||
|
var obj = new BibleVerses();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var http = _httpFactory.CreateClient();
|
||||||
|
obj = await http.GetFromJsonAsync<BibleVerses>($"https://bible-api.com/{book} {chapterAndVerse}");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.Error is not null || obj.Verses is null || obj.Verses.Length == 0)
|
||||||
|
await Response().Error(obj.Error ?? "No verse found.").SendAsync();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var v = obj.Verses[0];
|
||||||
|
await Response()
|
||||||
|
.Embed(_sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithTitle($"{v.BookName} {v.Chapter}:{v.Verse}")
|
||||||
|
.WithDescription(v.Text))
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
public async Task Quran(string ayah)
|
||||||
|
{
|
||||||
|
using var http = _httpFactory.CreateClient();
|
||||||
|
|
||||||
|
var obj = await http.GetFromJsonAsync<QuranResponse<QuranAyah>>($"https://api.alquran.cloud/v1/ayah/{Uri.EscapeDataString(ayah)}/editions/en.asad,ar.alafasy");
|
||||||
|
if(obj is null or not { Code: 200 })
|
||||||
|
{
|
||||||
|
await Response().Error("No verse found.").SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var english = obj.Data[0];
|
||||||
|
var arabic = obj.Data[1];
|
||||||
|
|
||||||
|
await using var audio = await http.GetStreamAsync(arabic.Audio);
|
||||||
|
|
||||||
|
await Response()
|
||||||
|
.Embed(_sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.AddField("Arabic", arabic.Text)
|
||||||
|
.AddField("English", english.Text)
|
||||||
|
.WithFooter(arabic.Number.ToString()))
|
||||||
|
.File(audio, Uri.EscapeDataString(ayah) + ".mp3")
|
||||||
|
.SendAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class QuranResponse<T>
|
||||||
|
{
|
||||||
|
[JsonPropertyName("code")]
|
||||||
|
public int Code { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public T[] Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class QuranAyah
|
||||||
|
{
|
||||||
|
[JsonPropertyName("number")]
|
||||||
|
public int Number { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("audio")]
|
||||||
|
public string Audio { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("text")]
|
||||||
|
public string Text { get; set; }
|
||||||
|
|
||||||
|
}
|
@@ -32,20 +32,21 @@ public sealed class SearxSearchService : SearchServiceBase, INService
|
|||||||
var instanceUrl = GetRandomInstance();
|
var instanceUrl = GetRandomInstance();
|
||||||
|
|
||||||
Log.Information("Using {Instance} instance for web search...", instanceUrl);
|
Log.Information("Using {Instance} instance for web search...", instanceUrl);
|
||||||
var sw = Stopwatch.StartNew();
|
var startTime = Stopwatch.GetTimestamp();
|
||||||
|
|
||||||
using var http = _http.CreateClient();
|
using var http = _http.CreateClient();
|
||||||
await using var res = await http.GetStreamAsync($"{instanceUrl}"
|
await using var res = await http.GetStreamAsync($"{instanceUrl}"
|
||||||
+ $"?q={Uri.EscapeDataString(query)}"
|
+ $"?q={Uri.EscapeDataString(query)}"
|
||||||
+ $"&format=json"
|
+ $"&format=json"
|
||||||
+ $"&strict=2");
|
+ $"&strict=2");
|
||||||
|
|
||||||
sw.Stop();
|
var elapsed = Stopwatch.GetElapsedTime(startTime);
|
||||||
var dat = await JsonSerializer.DeserializeAsync<SearxSearchResult>(res);
|
var dat = await JsonSerializer.DeserializeAsync<SearxSearchResult>(res);
|
||||||
|
|
||||||
if (dat is null)
|
if (dat is null)
|
||||||
return new SearxSearchResult();
|
return new SearxSearchResult();
|
||||||
|
|
||||||
dat.SearchTime = sw.Elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
|
dat.SearchTime = elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
|
||||||
return dat;
|
return dat;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ public sealed class SearxSearchService : SearchServiceBase, INService
|
|||||||
var instanceUrl = GetRandomInstance();
|
var instanceUrl = GetRandomInstance();
|
||||||
|
|
||||||
Log.Information("Using {Instance} instance for img search...", instanceUrl);
|
Log.Information("Using {Instance} instance for img search...", instanceUrl);
|
||||||
var sw = Stopwatch.StartNew();
|
var startTime = Stopwatch.GetTimestamp();
|
||||||
using var http = _http.CreateClient();
|
using var http = _http.CreateClient();
|
||||||
await using var res = await http.GetStreamAsync($"{instanceUrl}"
|
await using var res = await http.GetStreamAsync($"{instanceUrl}"
|
||||||
+ $"?q={Uri.EscapeDataString(query)}"
|
+ $"?q={Uri.EscapeDataString(query)}"
|
||||||
@@ -64,13 +65,13 @@ public sealed class SearxSearchService : SearchServiceBase, INService
|
|||||||
+ $"&category_images=on"
|
+ $"&category_images=on"
|
||||||
+ $"&strict=2");
|
+ $"&strict=2");
|
||||||
|
|
||||||
sw.Stop();
|
var elapsed = Stopwatch.GetElapsedTime(startTime);
|
||||||
var dat = await JsonSerializer.DeserializeAsync<SearxImageSearchResult>(res);
|
var dat = await JsonSerializer.DeserializeAsync<SearxImageSearchResult>(res);
|
||||||
|
|
||||||
if (dat is null)
|
if (dat is null)
|
||||||
return new SearxImageSearchResult();
|
return new SearxImageSearchResult();
|
||||||
|
|
||||||
dat.SearchTime = sw.Elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
|
dat.SearchTime = elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
|
||||||
return dat;
|
return dat;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -2,6 +2,7 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using NadekoBot.Modules.Searches.Common;
|
using NadekoBot.Modules.Searches.Common;
|
||||||
using NadekoBot.Modules.Searches.Services;
|
using NadekoBot.Modules.Searches.Services;
|
||||||
|
using NadekoBot.Modules.Utility;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
@@ -168,7 +169,7 @@ public partial class Searches : NadekoModule<SearchesService>
|
|||||||
.AddField("Rating", movie.ImdbRating, true)
|
.AddField("Rating", movie.ImdbRating, true)
|
||||||
.AddField("Genre", movie.Genre, true)
|
.AddField("Genre", movie.Genre, true)
|
||||||
.AddField("Year", movie.Year, true)
|
.AddField("Year", movie.Year, true)
|
||||||
.WithImageUrl(movie.Poster))
|
.WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute) ? movie.Poster : null))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,34 +528,6 @@ public partial class Searches : NadekoModule<SearchesService>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
|
||||||
[RequireContext(ContextType.Guild)]
|
|
||||||
public async Task Bible(string book, string chapterAndVerse)
|
|
||||||
{
|
|
||||||
var obj = new BibleVerses();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var http = _httpFactory.CreateClient();
|
|
||||||
obj = await http.GetFromJsonAsync<BibleVerses>($"https://bible-api.com/{book} {chapterAndVerse}");
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj.Error is not null || obj.Verses is null || obj.Verses.Length == 0)
|
|
||||||
await Response().Error(obj.Error ?? "No verse found.").SendAsync();
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var v = obj.Verses[0];
|
|
||||||
await Response()
|
|
||||||
.Embed(_sender.CreateEmbed()
|
|
||||||
.WithOkColor()
|
|
||||||
.WithTitle($"{v.BookName} {v.Chapter}:{v.Verse}")
|
|
||||||
.WithDescription(v.Text))
|
|
||||||
.SendAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
public async Task Steam([Leftover] string query)
|
public async Task Steam([Leftover] string query)
|
||||||
{
|
{
|
||||||
|
314
src/NadekoBot/Modules/Utility/Ai/AiAssistantService.cs
Normal file
314
src/NadekoBot/Modules/Utility/Ai/AiAssistantService.cs
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
using NadekoBot.Common.ModuleBehaviors;
|
||||||
|
using NadekoBot.Modules.Administration;
|
||||||
|
using NadekoBot.Modules.Games.Services;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Utility;
|
||||||
|
|
||||||
|
public enum GetCommandErrorResult
|
||||||
|
{
|
||||||
|
RateLimitHit,
|
||||||
|
NotAuthorized,
|
||||||
|
Disregard,
|
||||||
|
Unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class AiAssistantService
|
||||||
|
: IAiAssistantService, IReadyExecutor,
|
||||||
|
IExecOnMessage,
|
||||||
|
INService
|
||||||
|
{
|
||||||
|
private IReadOnlyCollection<AiCommandModel> _commands = [];
|
||||||
|
|
||||||
|
private readonly IBotStrings _strings;
|
||||||
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
|
private readonly CommandService _cmds;
|
||||||
|
private readonly IBotCredsProvider _credsProvider;
|
||||||
|
private readonly DiscordSocketClient _client;
|
||||||
|
private readonly ICommandHandler _cmdHandler;
|
||||||
|
private readonly BotConfigService _bcs;
|
||||||
|
private readonly IMessageSenderService _sender;
|
||||||
|
|
||||||
|
private readonly JsonSerializerOptions _serializerOptions = new();
|
||||||
|
private readonly IPermissionChecker _permChecker;
|
||||||
|
private readonly IBotCache _botCache;
|
||||||
|
private readonly ChatterBotService _cbs;
|
||||||
|
|
||||||
|
public AiAssistantService(
|
||||||
|
DiscordSocketClient client,
|
||||||
|
IBotStrings strings,
|
||||||
|
IHttpClientFactory httpFactory,
|
||||||
|
CommandService cmds,
|
||||||
|
IBotCredsProvider credsProvider,
|
||||||
|
ICommandHandler cmdHandler,
|
||||||
|
BotConfigService bcs,
|
||||||
|
IPermissionChecker permChecker,
|
||||||
|
IBotCache botCache,
|
||||||
|
ChatterBotService cbs,
|
||||||
|
IMessageSenderService sender)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_strings = strings;
|
||||||
|
_httpFactory = httpFactory;
|
||||||
|
_cmds = cmds;
|
||||||
|
_credsProvider = credsProvider;
|
||||||
|
_cmdHandler = cmdHandler;
|
||||||
|
_bcs = bcs;
|
||||||
|
_sender = sender;
|
||||||
|
_permChecker = permChecker;
|
||||||
|
_botCache = botCache;
|
||||||
|
_cbs = cbs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OneOf.OneOf<NadekoCommandCallModel, GetCommandErrorResult>> TryGetCommandAsync(
|
||||||
|
ulong userId,
|
||||||
|
string prompt,
|
||||||
|
IReadOnlyCollection<AiCommandModel> commands,
|
||||||
|
string prefix)
|
||||||
|
{
|
||||||
|
using var content = new StringContent(
|
||||||
|
JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
query = prompt,
|
||||||
|
commands = commands.ToDictionary(x => x.Name,
|
||||||
|
x => new AiCommandModel()
|
||||||
|
{
|
||||||
|
Desc = string.Format(x.Desc ?? "", prefix),
|
||||||
|
Params = x.Params,
|
||||||
|
Name = x.Name
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Encoding.UTF8,
|
||||||
|
"application/json"
|
||||||
|
);
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage();
|
||||||
|
request.Method = HttpMethod.Post;
|
||||||
|
// request.RequestUri = new("https://nai.nadeko.bot/get-command");
|
||||||
|
request.RequestUri = new("https://nai.nadeko.bot/get-command");
|
||||||
|
request.Content = content;
|
||||||
|
|
||||||
|
var creds = _credsProvider.GetCreds();
|
||||||
|
|
||||||
|
request.Headers.TryAddWithoutValidation("x-auth-token", creds.NadekoAiToken);
|
||||||
|
request.Headers.TryAddWithoutValidation("x-auth-userid", userId.ToString());
|
||||||
|
|
||||||
|
|
||||||
|
using var client = _httpFactory.CreateClient();
|
||||||
|
|
||||||
|
// todo customize according to the bot's config
|
||||||
|
// - CurrencyName
|
||||||
|
// -
|
||||||
|
|
||||||
|
using var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
if (response.StatusCode == HttpStatusCode.TooManyRequests)
|
||||||
|
{
|
||||||
|
return GetCommandErrorResult.RateLimitHit;
|
||||||
|
}
|
||||||
|
else if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
|
{
|
||||||
|
return GetCommandErrorResult.NotAuthorized;
|
||||||
|
}
|
||||||
|
|
||||||
|
var funcModel = await response.Content.ReadFromJsonAsync<CommandPromptResultModel>();
|
||||||
|
|
||||||
|
|
||||||
|
if (funcModel?.Name == "disregard")
|
||||||
|
{
|
||||||
|
Log.Warning("Disregarding the prompt: {Prompt}", prompt);
|
||||||
|
return GetCommandErrorResult.Disregard;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (funcModel is null)
|
||||||
|
return GetCommandErrorResult.Unknown;
|
||||||
|
|
||||||
|
var comModel = new NadekoCommandCallModel()
|
||||||
|
{
|
||||||
|
Name = funcModel.Name,
|
||||||
|
Arguments = funcModel.Arguments
|
||||||
|
.OrderBy(param => _commands.FirstOrDefault(x => x.Name == funcModel.Name)
|
||||||
|
?.Params
|
||||||
|
.Select((x, i) => (x, i))
|
||||||
|
.Where(x => x.x.Name == param.Key)
|
||||||
|
.Select(x => x.i)
|
||||||
|
.FirstOrDefault())
|
||||||
|
.Select(x => x.Value)
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.ToArray(),
|
||||||
|
Remaining = funcModel.Remaining
|
||||||
|
};
|
||||||
|
|
||||||
|
return comModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<AiCommandModel> GetCommands()
|
||||||
|
=> _commands;
|
||||||
|
|
||||||
|
public Task OnReadyAsync()
|
||||||
|
{
|
||||||
|
var cmds = _cmds.Commands
|
||||||
|
.Select(x => (MethodName: x.Summary, CommandName: x.Aliases[0]))
|
||||||
|
.Where(x => !x.MethodName.Contains("///"))
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var funcs = new List<AiCommandModel>();
|
||||||
|
foreach (var (method, cmd) in cmds)
|
||||||
|
{
|
||||||
|
var commandStrings = _strings.GetCommandStrings(method);
|
||||||
|
|
||||||
|
if (commandStrings is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
funcs.Add(new()
|
||||||
|
{
|
||||||
|
Name = cmd,
|
||||||
|
Desc = commandStrings?.Desc?.Replace("currency", "flowers") ?? string.Empty,
|
||||||
|
Params = commandStrings?.Params.FirstOrDefault()
|
||||||
|
?.Select(x => new AiCommandParamModel()
|
||||||
|
{
|
||||||
|
Desc = x.Value.Desc,
|
||||||
|
Name = x.Key,
|
||||||
|
})
|
||||||
|
.ToArray()
|
||||||
|
?? []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_commands = funcs;
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Priority
|
||||||
|
=> 2;
|
||||||
|
|
||||||
|
public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_credsProvider.GetCreds().NadekoAiToken))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (guild is not SocketGuild sg)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var nadekoId = _client.CurrentUser.Id;
|
||||||
|
|
||||||
|
var channel = msg.Channel as ITextChannel;
|
||||||
|
if (channel is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var normalMention = $"<@{nadekoId}> ";
|
||||||
|
var nickMention = $"<@!{nadekoId}> ";
|
||||||
|
string query;
|
||||||
|
if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture))
|
||||||
|
query = msg.Content[normalMention.Length..].Trim();
|
||||||
|
else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture))
|
||||||
|
query = msg.Content[nickMention.Length..].Trim();
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var success = await TryExecuteAiCommand(guild, msg, channel, query);
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> TryExecuteAiCommand(
|
||||||
|
IGuild guild,
|
||||||
|
IUserMessage msg,
|
||||||
|
ITextChannel channel,
|
||||||
|
string query)
|
||||||
|
{
|
||||||
|
// check permissions
|
||||||
|
var pcResult = await _permChecker.CheckPermsAsync(
|
||||||
|
guild,
|
||||||
|
msg.Channel,
|
||||||
|
msg.Author,
|
||||||
|
"Utility",
|
||||||
|
"prompt"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!pcResult.IsAllowed)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
using var _ = channel.EnterTypingState();
|
||||||
|
|
||||||
|
var result = await TryGetCommandAsync(msg.Author.Id, query, _commands, _cmdHandler.GetPrefix(guild.Id));
|
||||||
|
|
||||||
|
if (result.TryPickT0(out var model, out var error))
|
||||||
|
{
|
||||||
|
if (model.Name == ".ai_chat")
|
||||||
|
{
|
||||||
|
if (guild is not SocketGuild sg)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var sess = _cbs.GetOrCreateSession(guild.Id);
|
||||||
|
if (sess is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
await _cbs.RunChatterBot(sg, msg, channel, sess, query);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandString = GetCommandString(model);
|
||||||
|
|
||||||
|
var msgTask = _sender.Response(channel)
|
||||||
|
.Embed(_sender.CreateEmbed()
|
||||||
|
.WithOkColor()
|
||||||
|
.WithAuthor(msg.Author.GlobalName,
|
||||||
|
msg.Author.RealAvatarUrl().ToString())
|
||||||
|
.WithDescription(commandString))
|
||||||
|
.SendAsync();
|
||||||
|
|
||||||
|
|
||||||
|
await _cmdHandler.TryRunCommand(
|
||||||
|
(SocketGuild)guild,
|
||||||
|
(ISocketMessageChannel)channel,
|
||||||
|
new DoAsUserMessage((SocketUserMessage)msg, msg.Author, commandString));
|
||||||
|
|
||||||
|
var cmdMsg = await msgTask;
|
||||||
|
|
||||||
|
cmdMsg.DeleteAfter(5);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error == GetCommandErrorResult.Disregard)
|
||||||
|
{
|
||||||
|
// await msg.ErrorAsync();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = new TypedKey<bool>($"sub_error:{msg.Author.Id}:{error}");
|
||||||
|
|
||||||
|
if (!await _botCache.AddAsync(key, true, TimeSpan.FromDays(1), overwrite: false))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var errorMsg = error switch
|
||||||
|
{
|
||||||
|
GetCommandErrorResult.RateLimitHit
|
||||||
|
=> "You've spent your daily requests quota.",
|
||||||
|
GetCommandErrorResult.NotAuthorized
|
||||||
|
=> "In order to use this command you have to have a 5$ or higher subscription at <https://patreon.com/nadekobot>",
|
||||||
|
GetCommandErrorResult.Unknown
|
||||||
|
=> "The service is temporarily unavailable.",
|
||||||
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
|
};
|
||||||
|
|
||||||
|
await _sender.Response(channel)
|
||||||
|
.Error(errorMsg)
|
||||||
|
.SendAsync();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCommandString(NadekoCommandCallModel res)
|
||||||
|
=> $"{_bcs.Data.Prefix}{res.Name} {res.Arguments.Select((x, i) => GetParamString(x, i + 1 == res.Arguments.Count)).Join(" ")}";
|
||||||
|
|
||||||
|
private static string GetParamString(string val, bool isLast)
|
||||||
|
=> isLast ? val : "\"" + val + "\"";
|
||||||
|
}
|
15
src/NadekoBot/Modules/Utility/Ai/AiCommandModel.cs
Normal file
15
src/NadekoBot/Modules/Utility/Ai/AiCommandModel.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Utility;
|
||||||
|
|
||||||
|
public sealed class AiCommandModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("desc")]
|
||||||
|
public required string Desc { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("params")]
|
||||||
|
public required IReadOnlyList<AiCommandParamModel> Params { get; set; }
|
||||||
|
}
|
12
src/NadekoBot/Modules/Utility/Ai/AiCommandParamModel.cs
Normal file
12
src/NadekoBot/Modules/Utility/Ai/AiCommandParamModel.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Utility;
|
||||||
|
|
||||||
|
public sealed class AiCommandParamModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("desc")]
|
||||||
|
public required string Desc { get; set; }
|
||||||
|
}
|
16
src/NadekoBot/Modules/Utility/Ai/CommandPromptResultModel.cs
Normal file
16
src/NadekoBot/Modules/Utility/Ai/CommandPromptResultModel.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Utility;
|
||||||
|
|
||||||
|
public sealed class CommandPromptResultModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("arguments")]
|
||||||
|
public Dictionary<string, string> Arguments { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("remaining")]
|
||||||
|
[JsonConverter(typeof(NumberToStringConverter))]
|
||||||
|
public string Remaining { get; set; } = string.Empty;
|
||||||
|
}
|
20
src/NadekoBot/Modules/Utility/Ai/IAiAssistantService.cs
Normal file
20
src/NadekoBot/Modules/Utility/Ai/IAiAssistantService.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using OneOf;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Utility;
|
||||||
|
|
||||||
|
public interface IAiAssistantService
|
||||||
|
{
|
||||||
|
Task<OneOf<NadekoCommandCallModel, GetCommandErrorResult>> TryGetCommandAsync(
|
||||||
|
ulong userId,
|
||||||
|
string prompt,
|
||||||
|
IReadOnlyCollection<AiCommandModel> commands,
|
||||||
|
string prefix);
|
||||||
|
|
||||||
|
IReadOnlyCollection<AiCommandModel> GetCommands();
|
||||||
|
|
||||||
|
Task<bool> TryExecuteAiCommand(
|
||||||
|
IGuild guild,
|
||||||
|
IUserMessage msg,
|
||||||
|
ITextChannel channel,
|
||||||
|
string query);
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
namespace NadekoBot.Modules.Utility;
|
||||||
|
|
||||||
|
public sealed class NadekoCommandCallModel
|
||||||
|
{
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public required IReadOnlyList<string> Arguments { get; set; }
|
||||||
|
public required string Remaining { get; set; }
|
||||||
|
}
|
23
src/NadekoBot/Modules/Utility/Ai/UtilityCommands.cs
Normal file
23
src/NadekoBot/Modules/Utility/Ai/UtilityCommands.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using NadekoBot.Modules.Administration;
|
||||||
|
|
||||||
|
namespace NadekoBot.Modules.Utility;
|
||||||
|
|
||||||
|
public partial class UtilityCommands
|
||||||
|
{
|
||||||
|
public class PromptCommands : NadekoModule<IAiAssistantService>
|
||||||
|
{
|
||||||
|
[Cmd]
|
||||||
|
[RequireContext(ContextType.Guild)]
|
||||||
|
public async Task Prompt([Leftover] string query)
|
||||||
|
{
|
||||||
|
await ctx.Channel.TriggerTypingAsync();
|
||||||
|
var res = await _service.TryExecuteAiCommand(ctx.Guild, ctx.Message, (ITextChannel)ctx.Channel, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCommandString(NadekoCommandCallModel res)
|
||||||
|
=> $"{_bcs.Data.Prefix}{res.Name} {res.Arguments.Select((x, i) => GetParamString(x, i + 1 == res.Arguments.Count)).Join(" ")}";
|
||||||
|
|
||||||
|
private static string GetParamString(string val, bool isLast)
|
||||||
|
=> isLast ? val : "\"" + val + "\"";
|
||||||
|
}
|
||||||
|
}
|
@@ -144,9 +144,9 @@ public partial class Utility
|
|||||||
true)
|
true)
|
||||||
.WithOkColor();
|
.WithOkColor();
|
||||||
|
|
||||||
var patron = await _ps.GetPatronAsync(user.Id);
|
var mPatron = await _ps.GetPatronAsync(user.Id);
|
||||||
|
|
||||||
if (patron.Tier != PatronTier.None)
|
if (mPatron is {} patron && patron.Tier != PatronTier.None)
|
||||||
{
|
{
|
||||||
embed.WithFooter(patron.Tier switch
|
embed.WithFooter(patron.Tier switch
|
||||||
{
|
{
|
||||||
|
@@ -113,10 +113,9 @@ public partial class Utility
|
|||||||
foreach (var rem in rems)
|
foreach (var rem in rems)
|
||||||
{
|
{
|
||||||
var when = rem.When;
|
var when = rem.When;
|
||||||
var diff = when - DateTime.UtcNow;
|
|
||||||
embed.AddField(
|
embed.AddField(
|
||||||
$"#{++i + (page * 10)} {rem.When:HH:mm yyyy-MM-dd} UTC "
|
$"#{++i + (page * 10)} {rem.When:HH:mm yyyy-MM-dd} UTC "
|
||||||
+ $"(in {diff.ToPrettyStringHm()})",
|
+ $"{TimestampTag.FromDateTime(when)}",
|
||||||
$@"`Target:` {(rem.IsPrivate ? "DM" : "Channel")}
|
$@"`Target:` {(rem.IsPrivate ? "DM" : "Channel")}
|
||||||
`TargetId:` {rem.ChannelId}
|
`TargetId:` {rem.ChannelId}
|
||||||
`Message:` {rem.Message?.TrimTo(50)}");
|
`Message:` {rem.Message?.TrimTo(50)}");
|
||||||
@@ -203,16 +202,15 @@ public partial class Utility
|
|||||||
await uow.SaveChangesAsync();
|
await uow.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
var gTime = ctx.Guild is null ? time : TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(ctx.Guild.Id));
|
// var gTime = ctx.Guild is null ? time : TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(ctx.Guild.Id));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm($"\u23f0 {GetText(strs.remind(
|
.Confirm($"\u23f0 {GetText(strs.remind2(
|
||||||
Format.Bold(!isPrivate ? $"<#{targetId}>" : ctx.User.Username),
|
Format.Bold(!isPrivate ? $"<#{targetId}>" : ctx.User.Username),
|
||||||
Format.Bold(message),
|
Format.Bold(message),
|
||||||
ts.ToPrettyStringHm(),
|
TimestampTag.FromDateTime(DateTime.UtcNow.Add(ts), TimestampTagStyles.Relative),
|
||||||
gTime,
|
TimestampTag.FormatFromDateTime(time, TimestampTagStyles.ShortDateTime)))}")
|
||||||
gTime))}")
|
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
@@ -459,8 +459,10 @@ public partial class Utility : NadekoModule
|
|||||||
public async Task StickerAdd(string name = null, string description = null, params string[] tags)
|
public async Task StickerAdd(string name = null, string description = null, params string[] tags)
|
||||||
{
|
{
|
||||||
string format;
|
string format;
|
||||||
Stream stream;
|
Stream stream = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
if (ctx.Message.Stickers.Count is 1 && ctx.Message.Stickers.First() is SocketSticker ss)
|
if (ctx.Message.Stickers.Count is 1 && ctx.Message.Stickers.First() is SocketSticker ss)
|
||||||
{
|
{
|
||||||
name ??= ss.Name;
|
name ??= ss.Name;
|
||||||
@@ -471,6 +473,40 @@ public partial class Utility : NadekoModule
|
|||||||
using var http = _httpFactory.CreateClient();
|
using var http = _httpFactory.CreateClient();
|
||||||
stream = await http.GetStreamAsync(ss.GetStickerUrl());
|
stream = await http.GetStreamAsync(ss.GetStickerUrl());
|
||||||
}
|
}
|
||||||
|
else if (ctx.Message.Attachments.Count is 1 && name is not null)
|
||||||
|
{
|
||||||
|
if (tags.Length == 0)
|
||||||
|
tags = [name];
|
||||||
|
|
||||||
|
if (ctx.Message.Attachments.Count != 1)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.sticker_error).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attach = ctx.Message.Attachments.First();
|
||||||
|
|
||||||
|
|
||||||
|
if (attach.Size > 512_000 || attach.Width != 300 || attach.Height != 300)
|
||||||
|
{
|
||||||
|
await Response().Error(strs.sticker_error).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
format = attach.Filename
|
||||||
|
.Split('.')
|
||||||
|
.Last()
|
||||||
|
.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(format) || (format != "png" && format != "apng"))
|
||||||
|
{
|
||||||
|
await Response().Error(strs.sticker_error).SendAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var http = _httpFactory.CreateClient();
|
||||||
|
stream = await http.GetStreamAsync(attach.Url);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await Response().Error(strs.sticker_error).SendAsync();
|
await Response().Error(strs.sticker_error).SendAsync();
|
||||||
@@ -479,9 +515,6 @@ public partial class Utility : NadekoModule
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (tags.Length == 0)
|
|
||||||
tags = [name];
|
|
||||||
|
|
||||||
await ctx.Guild.CreateStickerAsync(
|
await ctx.Guild.CreateStickerAsync(
|
||||||
name,
|
name,
|
||||||
stream,
|
stream,
|
||||||
@@ -492,14 +525,16 @@ public partial class Utility : NadekoModule
|
|||||||
|
|
||||||
await ctx.OkAsync();
|
await ctx.OkAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch
|
||||||
|
(Exception ex)
|
||||||
{
|
{
|
||||||
Log.Warning(ex, "Error occurred while adding a sticker: {Message}", ex.Message);
|
Log.Warning(ex, "Error occurred while adding a sticker: {Message}", ex.Message);
|
||||||
await Response().Error(strs.error_occured).SendAsync();
|
await Response().Error(strs.error_occured).SendAsync();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
await stream.DisposeAsync();
|
await (stream?.DisposeAsync() ?? ValueTask.CompletedTask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -123,6 +123,8 @@ public partial class Xp
|
|||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
var rank = await _service.GetClubRankAsync(club.Id);
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Paginated()
|
.Paginated()
|
||||||
.Items(allUsers)
|
.Items(allUsers)
|
||||||
@@ -135,6 +137,7 @@ public partial class Xp
|
|||||||
.WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)")))
|
.WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)")))
|
||||||
.AddField(GetText(strs.desc),
|
.AddField(GetText(strs.desc),
|
||||||
string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description)
|
string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description)
|
||||||
|
.AddField(GetText(strs.rank), $"#{rank}", true)
|
||||||
.AddField(GetText(strs.owner), club.Owner.ToString(), true)
|
.AddField(GetText(strs.owner), club.Owner.ToString(), true)
|
||||||
// .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true)
|
// .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true)
|
||||||
.AddField(GetText(strs.members),
|
.AddField(GetText(strs.members),
|
||||||
|
@@ -134,6 +134,18 @@ public class ClubService : INService, IClubService
|
|||||||
return club is not null;
|
return club is not null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetClubRankAsync(int clubId)
|
||||||
|
{
|
||||||
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
|
var rank = await uow.Clubs
|
||||||
|
.ToLinqToDBTable()
|
||||||
|
.Where(x => x.Xp > (uow.Clubs.First(c => c.Id == clubId).Xp))
|
||||||
|
.CountAsyncLinqToDB();
|
||||||
|
|
||||||
|
return rank + 1;
|
||||||
|
}
|
||||||
|
|
||||||
public ClubApplyResult ApplyToClub(IUser user, ClubInfo club)
|
public ClubApplyResult ApplyToClub(IUser user, ClubInfo club)
|
||||||
{
|
{
|
||||||
using var uow = _db.GetDbContext();
|
using var uow = _db.GetDbContext();
|
||||||
|
@@ -23,6 +23,7 @@ public interface IClubService
|
|||||||
ClubKickResult Kick(ulong kickerId, string userName, out ClubInfo club);
|
ClubKickResult Kick(ulong kickerId, string userName, out ClubInfo club);
|
||||||
List<ClubInfo> GetClubLeaderboardPage(int page);
|
List<ClubInfo> GetClubLeaderboardPage(int page);
|
||||||
Task<ClubRenameResult> RenameClubAsync(ulong userId, string clubName);
|
Task<ClubRenameResult> RenameClubAsync(ulong userId, string clubName);
|
||||||
|
Task<int> GetClubRankAsync(int clubId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ClubApplyResult
|
public enum ClubApplyResult
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
using NadekoBot.Modules.Xp.Services;
|
using NadekoBot.Modules.Xp.Services;
|
||||||
using NadekoBot.Db.Models;
|
using NadekoBot.Db.Models;
|
||||||
using NadekoBot.Modules.Patronage;
|
using NadekoBot.Modules.Patronage;
|
||||||
|
using NadekoBot.Modules.Utility;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Xp;
|
namespace NadekoBot.Modules.Xp;
|
||||||
|
|
||||||
@@ -479,7 +480,8 @@ public partial class Xp : NadekoModule<XpService>
|
|||||||
ctx.User.Id,
|
ctx.User.Id,
|
||||||
button,
|
button,
|
||||||
OnShopUse,
|
OnShopUse,
|
||||||
(key, itemType));
|
(key, itemType),
|
||||||
|
clearAfter: false);
|
||||||
|
|
||||||
return inter;
|
return inter;
|
||||||
}
|
}
|
||||||
@@ -493,7 +495,9 @@ public partial class Xp : NadekoModule<XpService>
|
|||||||
ctx.User.Id,
|
ctx.User.Id,
|
||||||
button,
|
button,
|
||||||
OnShopBuy,
|
OnShopBuy,
|
||||||
(key, itemType));
|
(key, itemType),
|
||||||
|
singleUse: true,
|
||||||
|
clearAfter: false);
|
||||||
|
|
||||||
return inter;
|
return inter;
|
||||||
}
|
}
|
||||||
@@ -576,6 +580,10 @@ public partial class Xp : NadekoModule<XpService>
|
|||||||
{
|
{
|
||||||
await Response().Error(strs.not_enough(_gss.GetCurrencySign())).SendAsync();
|
await Response().Error(strs.not_enough(_gss.GetCurrencySign())).SendAsync();
|
||||||
}
|
}
|
||||||
|
else if (result == BuyResult.Success)
|
||||||
|
{
|
||||||
|
await _service.UseShopItemAsync(ctx.User.Id, type, key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetNotifLocationString(XpNotificationLocation loc)
|
private string GetNotifLocationString(XpNotificationLocation loc)
|
||||||
|
@@ -668,7 +668,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
/// <param name="channel"></param>
|
/// <param name="channel"></param>
|
||||||
private async Task ScanUserForVoiceXp(SocketGuildUser user, SocketVoiceChannel channel)
|
private async Task ScanUserForVoiceXp(SocketGuildUser user, SocketVoiceChannel channel)
|
||||||
{
|
{
|
||||||
if (UserParticipatingInVoiceChannel(user) && ShouldTrackXp(user, channel.Id))
|
if (UserParticipatingInVoiceChannel(user) && ShouldTrackXp(user, channel))
|
||||||
await UserJoinedVoiceChannel(user);
|
await UserJoinedVoiceChannel(user);
|
||||||
else
|
else
|
||||||
await UserLeftVoiceChannel(user, channel);
|
await UserLeftVoiceChannel(user, channel);
|
||||||
@@ -767,9 +767,12 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private bool ShouldTrackXp(SocketGuildUser user, ulong channelId)
|
private bool ShouldTrackXp(SocketGuildUser user, IMessageChannel channel)
|
||||||
{
|
{
|
||||||
if (_excludedChannels.TryGetValue(user.Guild.Id, out var chans) && chans.Contains(channelId))
|
var channelId = channel.Id;
|
||||||
|
|
||||||
|
if (_excludedChannels.TryGetValue(user.Guild.Id, out var chans) && (chans.Contains(channelId)
|
||||||
|
|| (channel is SocketThreadChannel tc && chans.Contains(tc.ParentChannel.Id))))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (_excludedServers.Contains(user.Guild.Id))
|
if (_excludedServers.Contains(user.Guild.Id))
|
||||||
@@ -788,7 +791,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
if (!ShouldTrackXp(user, arg.Channel.Id))
|
if (!ShouldTrackXp(user, arg.Channel))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var xpConf = _xpConfig.Data;
|
var xpConf = _xpConfig.Data;
|
||||||
@@ -1286,9 +1289,9 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
Image? frame = null;
|
Image? frame = null;
|
||||||
if (item is null)
|
if (item is null)
|
||||||
{
|
{
|
||||||
if (patron.Tier == PatronTier.V)
|
if (patron?.Tier == PatronTier.V)
|
||||||
frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_silver.png"));
|
frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_silver.png"));
|
||||||
else if (patron.Tier >= PatronTier.X || _creds.IsOwner(userId))
|
else if (patron?.Tier >= PatronTier.X || _creds.IsOwner(userId))
|
||||||
frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_gold.png"));
|
frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_gold.png"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1465,7 +1468,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
|||||||
{
|
{
|
||||||
var patron = await _ps.GetPatronAsync(userId);
|
var patron = await _ps.GetPatronAsync(userId);
|
||||||
|
|
||||||
if ((int)patron.Tier < (int)req)
|
if (patron is null || (int)patron.Value.Tier < (int)req)
|
||||||
return BuyResult.InsufficientPatronTier;
|
return BuyResult.InsufficientPatronTier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>true</ImplicitUsings>
|
<ImplicitUsings>true</ImplicitUsings>
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
<Version>5.0.7</Version>
|
<Version>5.1.4</Version>
|
||||||
|
|
||||||
<!-- Output/build -->
|
<!-- Output/build -->
|
||||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||||
@@ -73,9 +73,9 @@
|
|||||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.8"/>
|
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.8"/>
|
||||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14"/>
|
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14"/>
|
||||||
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009"/>
|
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009"/>
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.7.33"/>
|
<PackageReference Include="StackExchange.Redis" Version="2.8.0" />
|
||||||
<PackageReference Include="YamlDotNet" Version="15.1.4"/>
|
<PackageReference Include="YamlDotNet" Version="15.1.4"/>
|
||||||
<PackageReference Include="SharpToken" Version="2.0.2"/>
|
<PackageReference Include="SharpToken" Version="2.0.3" />
|
||||||
|
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0"/>
|
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0"/>
|
||||||
|
|
||||||
|
@@ -133,52 +133,8 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
|||||||
File.WriteAllText(CREDS_FILE_NAME, ymlData);
|
File.WriteAllText(CREDS_FILE_NAME, ymlData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string OldCredsJsonPath
|
|
||||||
=> Path.Combine(Directory.GetCurrentDirectory(), "credentials.json");
|
|
||||||
|
|
||||||
private string OldCredsJsonBackupPath
|
|
||||||
=> Path.Combine(Directory.GetCurrentDirectory(), "credentials.json.bak");
|
|
||||||
|
|
||||||
private void MigrateCredentials()
|
private void MigrateCredentials()
|
||||||
{
|
{
|
||||||
if (File.Exists(OldCredsJsonPath))
|
|
||||||
{
|
|
||||||
Log.Information("Migrating old creds...");
|
|
||||||
var jsonCredentialsFileText = File.ReadAllText(OldCredsJsonPath);
|
|
||||||
var oldCreds = JsonConvert.DeserializeObject<OldCreds>(jsonCredentialsFileText);
|
|
||||||
|
|
||||||
if (oldCreds is null)
|
|
||||||
{
|
|
||||||
Log.Error("Error while reading old credentials file. Make sure that the file is formatted correctly");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var creds = new Creds
|
|
||||||
{
|
|
||||||
Version = 1,
|
|
||||||
Token = oldCreds.Token,
|
|
||||||
OwnerIds = oldCreds.OwnerIds.Distinct().ToHashSet(),
|
|
||||||
GoogleApiKey = oldCreds.GoogleApiKey,
|
|
||||||
RapidApiKey = oldCreds.MashapeKey,
|
|
||||||
OsuApiKey = oldCreds.OsuApiKey,
|
|
||||||
CleverbotApiKey = oldCreds.CleverbotApiKey,
|
|
||||||
TotalShards = oldCreds.TotalShards <= 1 ? 1 : oldCreds.TotalShards,
|
|
||||||
Patreon = new Creds.PatreonSettings(oldCreds.PatreonAccessToken, null, null, oldCreds.PatreonCampaignId),
|
|
||||||
Votes = new Creds.VotesSettings(oldCreds.VotesUrl, oldCreds.VotesToken, string.Empty, string.Empty),
|
|
||||||
BotListToken = oldCreds.BotListToken,
|
|
||||||
RedisOptions = oldCreds.RedisOptions,
|
|
||||||
LocationIqApiKey = oldCreds.LocationIqApiKey,
|
|
||||||
TimezoneDbApiKey = oldCreds.TimezoneDbApiKey,
|
|
||||||
CoinmarketcapApiKey = oldCreds.CoinmarketcapApiKey
|
|
||||||
};
|
|
||||||
|
|
||||||
File.Move(OldCredsJsonPath, OldCredsJsonBackupPath, true);
|
|
||||||
File.WriteAllText(CredsPath, Yaml.Serializer.Serialize(creds));
|
|
||||||
|
|
||||||
Log.Warning(
|
|
||||||
"Data from credentials.json has been moved to creds.yml\nPlease inspect your creds.yml for correctness");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (File.Exists(CREDS_FILE_NAME))
|
if (File.Exists(CREDS_FILE_NAME))
|
||||||
{
|
{
|
||||||
var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(CREDS_FILE_NAME));
|
var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(CREDS_FILE_NAME));
|
||||||
@@ -191,6 +147,12 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
|||||||
creds.Version = 7;
|
creds.Version = 7;
|
||||||
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
|
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (creds.Version <= 8)
|
||||||
|
{
|
||||||
|
creds.Version = 9;
|
||||||
|
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@ using OneOf.Types;
|
|||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using YamlDotNet.Core.Tokens;
|
||||||
|
|
||||||
namespace NadekoBot.Common;
|
namespace NadekoBot.Common;
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ public sealed class RedisBotCache : IBotCache
|
|||||||
var success = await db.StringSetAsync(key.Key,
|
var success = await db.StringSetAsync(key.Key,
|
||||||
val,
|
val,
|
||||||
expiry: expiry,
|
expiry: expiry,
|
||||||
|
keepTtl: true,
|
||||||
when: overwrite ? When.Always : When.NotExists);
|
when: overwrite ? When.Always : When.NotExists);
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
|
@@ -147,4 +147,5 @@ public static class StringExtensions
|
|||||||
var newString = str.UnescapeUnicodeCodePoint();
|
var newString = str.UnescapeUnicodeCodePoint();
|
||||||
return newString;
|
return newString;
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
@@ -4,8 +4,9 @@ namespace NadekoBot;
|
|||||||
public interface IBotCredentials
|
public interface IBotCredentials
|
||||||
{
|
{
|
||||||
string Token { get; }
|
string Token { get; }
|
||||||
string GoogleApiKey { get; }
|
string NadekoAiToken { get; }
|
||||||
ICollection<ulong> OwnerIds { get; set; }
|
ICollection<ulong> OwnerIds { get; set; }
|
||||||
|
string GoogleApiKey { get; }
|
||||||
bool UsePrivilegedIntents { get; }
|
bool UsePrivilegedIntents { get; }
|
||||||
string RapidApiKey { get; }
|
string RapidApiKey { get; }
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user