mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-10 17:28:27 -04:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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:
|
||||
stage: build
|
||||
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"
|
||||
- "dotnet publish -c Release -r win-x64 --self-contained -o $WIN_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
||||
- "dotnet publish -c Release -r win-arm64 --self-contained -o $WIN_ARM64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
||||
- "dotnet publish -c Release -r osx-x64 --self-contained -o $MACOS_X64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
||||
- "dotnet publish -c Release -r osx-arm64 --self-contained -o $MACOS_ARM64_OUTPUT_DIR src/NadekoBot/NadekoBot.csproj"
|
||||
- |
|
||||
VERSION_STRING=""
|
||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||
VERSION_STRING="-p:Version=$CI_COMMIT_TAG"
|
||||
fi
|
||||
- "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:
|
||||
paths:
|
||||
- "$LINUX_X64_OUTPUT_DIR/"
|
||||
|
54
CHANGELOG.md
54
CHANGELOG.md
@@ -2,6 +2,60 @@
|
||||
|
||||
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
|
||||
|
||||
## [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
|
||||
|
||||
### Added
|
||||
|
@@ -93,7 +93,7 @@ public sealed class Bot : IBot
|
||||
private void AddServices()
|
||||
{
|
||||
var startingGuildIdList = GetCurrentGuildIds();
|
||||
var sw = Stopwatch.StartNew();
|
||||
var startTime = Stopwatch.GetTimestamp();
|
||||
var bot = Client.CurrentUser;
|
||||
|
||||
using (var uow = _db.GetDbContext())
|
||||
@@ -160,9 +160,8 @@ public sealed class Bot : IBot
|
||||
{
|
||||
LoadTypeReaders(a);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Log.Information("All services loaded in {ServiceLoadTime:F2}s", sw.Elapsed.TotalSeconds);
|
||||
|
||||
Log.Information("All services loaded in {ServiceLoadTime:F2}s", Stopwatch.GetElapsedTime(startTime) .TotalSeconds);
|
||||
}
|
||||
|
||||
private void LoadTypeReaders(Assembly assembly)
|
||||
@@ -259,7 +258,7 @@ public sealed class Bot : IBot
|
||||
if (ShardId == 0)
|
||||
await _db.SetupAsync();
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var startTime = Stopwatch.GetTimestamp();
|
||||
|
||||
await LoginAsync(_creds.Token);
|
||||
|
||||
@@ -274,8 +273,7 @@ public sealed class Bot : IBot
|
||||
Helpers.ReadErrorAndExit(9);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s", Client.ShardId, sw.Elapsed.TotalSeconds);
|
||||
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s", Client.ShardId, Stopwatch.GetElapsedTime(startTime).TotalSeconds);
|
||||
var commandHandler = Services.GetRequiredService<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
|
||||
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 string UniquePlatformUserId { get; set; }
|
||||
|
@@ -53,14 +53,13 @@ public abstract class NadekoContext : DbContext
|
||||
|
||||
public DbSet<PatronUser> Patrons { get; set; }
|
||||
|
||||
public DbSet<PatronQuota> PatronQuotas { get; set; }
|
||||
|
||||
public DbSet<StreamOnlineMessage> StreamOnlineMessages { get; set; }
|
||||
|
||||
public DbSet<StickyRole> StickyRoles { get; set; }
|
||||
|
||||
public DbSet<TodoModel> Todos { get; set; }
|
||||
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
|
||||
public DbSet<HoneypotChannel> HoneyPotChannels { get; set; }
|
||||
|
||||
// todo add guild colors
|
||||
// public DbSet<GuildColors> GuildColors { get; set; }
|
||||
@@ -597,16 +596,6 @@ public abstract class NadekoContext : DbContext
|
||||
});
|
||||
|
||||
// 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
|
||||
|
||||
|
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);
|
||||
});
|
||||
|
||||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1718,41 +1737,6 @@ namespace NadekoBot.Migrations.Mysql
|
||||
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 =>
|
||||
{
|
||||
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);
|
||||
});
|
||||
|
||||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1717,41 +1734,6 @@ namespace NadekoBot.Migrations.PostgreSql
|
||||
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 =>
|
||||
{
|
||||
b.Property<decimal>("UserId")
|
||||
|
@@ -11,6 +11,8 @@ namespace NadekoBot.Migrations
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
MigrationQueries.GuildConfigCleanup(migrationBuilder);
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_AntiRaidSetting_GuildConfigs_GuildConfigId",
|
||||
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");
|
||||
});
|
||||
|
||||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1279,33 +1293,6 @@ namespace NadekoBot.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.Property<ulong>("UserId")
|
||||
|
@@ -61,17 +61,66 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, INService
|
||||
}));
|
||||
}
|
||||
|
||||
// delete guild configs
|
||||
await ctx.GetTable<GuildConfig>()
|
||||
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId))
|
||||
.DeleteAsync();
|
||||
|
||||
|
||||
|
||||
// delete guild xp
|
||||
await ctx.GetTable<UserXpStats>()
|
||||
.Where(x => !tempTable.Select(x => x.GuildId)
|
||||
.Contains(x.GuildId))
|
||||
.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()
|
||||
{
|
||||
GuildCount = guildIds.Keys.Count,
|
||||
|
@@ -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 progress = GetProgressTracker(progressMsg);
|
||||
|
||||
PruneResult result;
|
||||
if (opts.Safe)
|
||||
await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
100,
|
||||
x => x.Author.Id == user.Id && !x.IsPinned,
|
||||
progress,
|
||||
opts.After);
|
||||
else
|
||||
await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
100,
|
||||
x => x.Author.Id == user.Id,
|
||||
progress,
|
||||
opts.After);
|
||||
|
||||
ctx.Message.DeleteAfter(3);
|
||||
|
||||
await SendResult(result);
|
||||
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
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
@@ -83,19 +103,21 @@ public partial class Administration
|
||||
var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync();
|
||||
var progress = GetProgressTracker(progressMsg);
|
||||
|
||||
PruneResult result;
|
||||
if (opts.Safe)
|
||||
await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
count,
|
||||
x => !x.IsPinned && x.Id != progressMsg.Id,
|
||||
progress,
|
||||
opts.After);
|
||||
else
|
||||
await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
count,
|
||||
x => x.Id != progressMsg.Id,
|
||||
progress,
|
||||
opts.After);
|
||||
|
||||
await SendResult(result);
|
||||
await progressMsg.DeleteAsync();
|
||||
}
|
||||
|
||||
@@ -155,9 +177,10 @@ public partial class Administration
|
||||
var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync();
|
||||
var progress = GetProgressTracker(progressMsg);
|
||||
|
||||
PruneResult result;
|
||||
if (opts.Safe)
|
||||
{
|
||||
await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
count,
|
||||
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned,
|
||||
progress,
|
||||
@@ -166,7 +189,7 @@ public partial class Administration
|
||||
}
|
||||
else
|
||||
{
|
||||
await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
|
||||
count,
|
||||
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks,
|
||||
progress,
|
||||
@@ -174,6 +197,7 @@ public partial class Administration
|
||||
);
|
||||
}
|
||||
|
||||
await SendResult(result);
|
||||
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
|
||||
using NadekoBot.Modules.Patronage;
|
||||
|
||||
namespace NadekoBot.Modules.Administration.Services;
|
||||
|
||||
public class PruneService : INService
|
||||
@@ -7,11 +9,15 @@ public class PruneService : INService
|
||||
private readonly ConcurrentDictionary<ulong, CancellationTokenSource> _pruningGuilds = new();
|
||||
private readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14);
|
||||
private readonly ILogCommandService _logService;
|
||||
private readonly IPatronageService _ps;
|
||||
|
||||
public PruneService(ILogCommandService logService)
|
||||
=> _logService = logService;
|
||||
public PruneService(ILogCommandService logService, IPatronageService ps)
|
||||
{
|
||||
_logService = logService;
|
||||
_ps = ps;
|
||||
}
|
||||
|
||||
public async Task PruneWhere(
|
||||
public async Task<PruneResult> PruneWhere(
|
||||
ITextChannel channel,
|
||||
int amount,
|
||||
Func<IMessage, bool> predicate,
|
||||
@@ -26,8 +32,13 @@ public class PruneService : INService
|
||||
|
||||
using var cancelSource = new CancellationTokenSource();
|
||||
if (!_pruningGuilds.TryAdd(channel.GuildId, cancelSource))
|
||||
return;
|
||||
return PruneResult.AlreadyRunning;
|
||||
|
||||
if (!await _ps.LimitHitAsync(LimitedFeatureName.Prune, channel.Guild.OwnerId))
|
||||
{
|
||||
return PruneResult.FeatureLimit;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
@@ -47,7 +58,7 @@ public class PruneService : INService
|
||||
.ToArray();
|
||||
|
||||
if (!msgs.Any())
|
||||
return;
|
||||
return PruneResult.Success;
|
||||
|
||||
lastMessage = msgs[^1];
|
||||
|
||||
@@ -88,6 +99,8 @@ public class PruneService : INService
|
||||
{
|
||||
_pruningGuilds.TryRemove(channel.GuildId, out _);
|
||||
}
|
||||
|
||||
return PruneResult.Success;
|
||||
}
|
||||
|
||||
public async Task<bool> CancelAsync(ulong guildId)
|
||||
|
@@ -18,7 +18,7 @@ public interface IReactionRoleService
|
||||
/// <param name="group"></param>
|
||||
/// <param name="levelReq"></param>
|
||||
/// <returns>The result of the operation</returns>
|
||||
Task<OneOf<Success, FeatureLimit>> AddReactionRole(
|
||||
Task<OneOf<Success, Error>> AddReactionRole(
|
||||
IGuild guild,
|
||||
IMessage msg,
|
||||
string emote,
|
||||
|
@@ -55,12 +55,10 @@ public partial class Administration
|
||||
|
||||
await res.Match(
|
||||
_ => ctx.OkAsync(),
|
||||
fl =>
|
||||
async fl =>
|
||||
{
|
||||
_ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
|
||||
return !fl.IsPatronLimit
|
||||
? Response().Error(strs.limit_reached(fl.Quota)).SendAsync()
|
||||
: Response().Pending(strs.feature_limit_reached_owner(fl.Quota, fl.Name)).SendAsync();
|
||||
await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -21,22 +21,16 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
|
||||
private readonly SemaphoreSlim _assignementLock = new(1, 1);
|
||||
private readonly IPatronageService _ps;
|
||||
|
||||
private static readonly FeatureLimitKey _reroFLKey = new()
|
||||
{
|
||||
Key = "rero:max_count",
|
||||
PrettyName = "Reaction Role"
|
||||
};
|
||||
|
||||
public ReactionRolesService(
|
||||
DiscordSocketClient client,
|
||||
IPatronageService ps,
|
||||
DbService db,
|
||||
IBotCredentials creds,
|
||||
IPatronageService ps)
|
||||
IBotCredentials creds)
|
||||
{
|
||||
_db = db;
|
||||
_ps = ps;
|
||||
_client = client;
|
||||
_creds = creds;
|
||||
_ps = ps;
|
||||
_cache = new();
|
||||
}
|
||||
|
||||
@@ -242,7 +236,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
|
||||
/// <param name="group"></param>
|
||||
/// <param name="levelReq"></param>
|
||||
/// <returns>The result of the operation</returns>
|
||||
public async Task<OneOf<Success, FeatureLimit>> AddReactionRole(
|
||||
public async Task<OneOf<Success, Error>> AddReactionRole(
|
||||
IGuild guild,
|
||||
IMessage msg,
|
||||
string emote,
|
||||
@@ -261,9 +255,12 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
|
||||
.Where(x => x.GuildId == guild.Id)
|
||||
.CountAsync();
|
||||
|
||||
var result = await _ps.TryGetFeatureLimitAsync(_reroFLKey, guild.OwnerId, 50);
|
||||
if (result.Quota != -1 && activeReactionRoles >= result.Quota)
|
||||
return result;
|
||||
var limit = await _ps.GetUserLimit(LimitedFeatureName.ReactionRole, guild.OwnerId);
|
||||
|
||||
if (!_creds.IsOwner(guild.OwnerId) && (activeReactionRoles >= limit.Quota && limit.Quota >= 0))
|
||||
{
|
||||
return new Error();
|
||||
}
|
||||
|
||||
await ctx.GetTable<ReactionRoleV2>()
|
||||
.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()
|
||||
// {
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// }
|
@@ -3,6 +3,7 @@ using NadekoBot.Common.TypeReaders;
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using NadekoBot.Modules.Gambling.Common.Blackjack;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using NadekoBot.Modules.Utility;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling;
|
||||
|
||||
|
@@ -17,7 +17,8 @@ public partial class Gambling
|
||||
private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new();
|
||||
private readonly IImageCache _images;
|
||||
|
||||
public DrawCommands(IImageCache images, GamblingConfigService gcs) : base(gcs)
|
||||
public DrawCommands(IImageCache images, GamblingConfigService gcs)
|
||||
: base(gcs)
|
||||
=> _images = images;
|
||||
|
||||
private async Task InternalDraw(int count, ulong? guildId = null)
|
||||
@@ -56,8 +57,8 @@ public partial class Gambling
|
||||
i.Dispose();
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
.WithOkColor();
|
||||
|
||||
.WithOkColor();
|
||||
|
||||
var toSend = string.Empty;
|
||||
if (cardObjects.Count == 5)
|
||||
eb.AddField(GetText(strs.hand_value), Deck.GetHandValue(cardObjects), true);
|
||||
@@ -71,7 +72,7 @@ public partial class Gambling
|
||||
|
||||
if (count > 1)
|
||||
eb.AddField(GetText(strs.cards), count.ToString(), true);
|
||||
|
||||
|
||||
await using var imageStream = await img.ToStreamAsync();
|
||||
await ctx.Channel.SendFileAsync(imageStream,
|
||||
imgName,
|
||||
@@ -84,7 +85,7 @@ public partial class Gambling
|
||||
var cardBytes = await File.ReadAllBytesAsync($"data/images/cards/{cardName}.jpg");
|
||||
return Image.Load<Rgba32>(cardBytes);
|
||||
}
|
||||
|
||||
|
||||
private async Task<Image<Rgba32>> GetCardImageAsync(Deck.Card currentCard)
|
||||
{
|
||||
var cardName = currentCard.ToString().ToLowerInvariant().Replace(' ', '_');
|
||||
@@ -98,7 +99,7 @@ public partial class Gambling
|
||||
{
|
||||
if (num < 1)
|
||||
return;
|
||||
|
||||
|
||||
if (num > 10)
|
||||
num = 10;
|
||||
|
||||
@@ -110,7 +111,7 @@ public partial class Gambling
|
||||
{
|
||||
if (num < 1)
|
||||
return;
|
||||
|
||||
|
||||
if (num > 10)
|
||||
num = 10;
|
||||
|
||||
@@ -136,19 +137,29 @@ public partial class Gambling
|
||||
|
||||
[Cmd]
|
||||
[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);
|
||||
|
||||
|
||||
[Cmd]
|
||||
[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);
|
||||
|
||||
|
||||
public async Task BetDrawInternal(long amount, InputValueGuess? val, InputColorGuess? col)
|
||||
{
|
||||
if (amount <= 0)
|
||||
if (!await CheckBetMandatory(amount))
|
||||
{
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
var res = await _service.BetDrawAsync(ctx.User.Id,
|
||||
amount,
|
||||
(byte?)val,
|
||||
@@ -161,13 +172,13 @@ public partial class Gambling
|
||||
}
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithDescription(result.Card.GetEmoji())
|
||||
.AddField(GetText(strs.guess), GetGuessInfo(val, col), true)
|
||||
.AddField(GetText(strs.card), GetCardInfo(result.Card), true)
|
||||
.AddField(GetText(strs.won), N((long)result.Won), false)
|
||||
.WithImageUrl("attachment://card.png");
|
||||
.WithOkColor()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithDescription(result.Card.GetEmoji())
|
||||
.AddField(GetText(strs.guess), GetGuessInfo(val, col), true)
|
||||
.AddField(GetText(strs.card), GetCardInfo(result.Card), true)
|
||||
.AddField(GetText(strs.won), N((long)result.Won), false)
|
||||
.WithImageUrl("attachment://card.png");
|
||||
|
||||
using var img = await GetCardImageAsync(result.Card);
|
||||
await using var imgStream = await img.ToStreamAsync();
|
||||
@@ -189,9 +200,10 @@ public partial class Gambling
|
||||
InputColorGuess.Black => "B ⚫",
|
||||
_ => "❓"
|
||||
};
|
||||
|
||||
|
||||
return $"{val} / {col}";
|
||||
}
|
||||
|
||||
private string GetCardInfo(RegularCard card)
|
||||
{
|
||||
var val = (int)card.Value switch
|
||||
@@ -208,7 +220,7 @@ public partial class Gambling
|
||||
RegularSuit.Diamonds or RegularSuit.Hearts => "R 🔴",
|
||||
_ => "B ⚫"
|
||||
};
|
||||
|
||||
|
||||
return $"{val} / {col}";
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
#nullable disable
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Db;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Gambling.Bank;
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
@@ -14,6 +13,7 @@ using System.Text;
|
||||
using NadekoBot.Modules.Gambling.Rps;
|
||||
using NadekoBot.Common.TypeReaders;
|
||||
using NadekoBot.Modules.Patronage;
|
||||
using NadekoBot.Modules.Utility;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling;
|
||||
|
||||
@@ -27,9 +27,9 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||
private readonly DownloadTracker _tracker;
|
||||
private readonly GamblingConfigService _configService;
|
||||
private readonly IBankService _bank;
|
||||
private readonly IPatronageService _ps;
|
||||
private readonly IRemindService _remind;
|
||||
private readonly GamblingTxTracker _gamblingTxTracker;
|
||||
private readonly IPatronageService _ps;
|
||||
|
||||
private IUserMessage rdMsg;
|
||||
|
||||
@@ -41,8 +41,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||
DownloadTracker tracker,
|
||||
GamblingConfigService configService,
|
||||
IBankService bank,
|
||||
IPatronageService ps,
|
||||
IRemindService remind,
|
||||
IPatronageService patronage,
|
||||
GamblingTxTracker gamblingTxTracker)
|
||||
: base(configService)
|
||||
{
|
||||
@@ -51,9 +51,9 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||
_cs = currency;
|
||||
_client = client;
|
||||
_bank = bank;
|
||||
_ps = ps;
|
||||
_remind = remind;
|
||||
_gamblingTxTracker = gamblingTxTracker;
|
||||
_ps = patronage;
|
||||
|
||||
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
|
||||
_enUsCulture.NumberDecimalDigits = 0;
|
||||
@@ -133,12 +133,6 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// Creates timely reminder button, parameter in hours.
|
||||
private NadekoInteractionBase CreateRemindMeInteraction(int period)
|
||||
=> _inter
|
||||
.Create(ctx.User.Id,
|
||||
@@ -163,6 +158,17 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||
customId: "timely:remind_me"),
|
||||
(smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromHours(period)))
|
||||
);
|
||||
|
||||
// Creates timely reminder button, parameter in milliseconds.
|
||||
private NadekoInteractionBase CreateRemindMeInteraction(double ms)
|
||||
=> _inter
|
||||
.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]
|
||||
public async Task Timely()
|
||||
@@ -175,25 +181,31 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||
return;
|
||||
}
|
||||
|
||||
var inter = CreateRemindMeInteraction(period);
|
||||
|
||||
if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } rem)
|
||||
if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } remainder)
|
||||
{
|
||||
// Get correct time form remainder
|
||||
var interaction = CreateRemindMeInteraction(remainder.TotalMilliseconds);
|
||||
|
||||
// Removes timely button if there is a timely reminder in DB
|
||||
if (_service.UserHasTimelyReminder(ctx.User.Id))
|
||||
{
|
||||
inter = null;
|
||||
interaction = null;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var relativeTag = TimestampTag.FromDateTime(now.Add(rem), TimestampTagStyles.Relative);
|
||||
await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(inter).SendAsync();
|
||||
var relativeTag = TimestampTag.FromDateTime(now.Add(remainder), TimestampTagStyles.Relative);
|
||||
await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(interaction).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var result = await _ps.TryGetFeatureLimitAsync(_timelyKey, ctx.User.Id, 0);
|
||||
var patron = await _ps.GetPatronAsync(ctx.User.Id);
|
||||
|
||||
val = (int)(val * (1 + (result.Quota! * 0.01f)));
|
||||
var percentBonus = (_ps.PercentBonus(patron) / 100f);
|
||||
|
||||
val += (int)(val * percentBonus);
|
||||
|
||||
var inter = CreateRemindMeInteraction(period);
|
||||
|
||||
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 =
|
||||
new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray();
|
||||
|
||||
|
||||
[Cmd]
|
||||
public async Task LuckyLadder([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
|
||||
{
|
||||
|
@@ -247,7 +247,14 @@ public partial class Gambling
|
||||
}
|
||||
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()
|
||||
.WithPendingColor()
|
||||
.WithTitle("Executing shop command")
|
||||
@@ -259,6 +266,7 @@ public partial class Gambling
|
||||
GetProfitAmount(entry.Price),
|
||||
new("shop", "sell", entry.Name));
|
||||
|
||||
await Task.Delay(250);
|
||||
await _cmdHandler.TryRunCommand(guild,
|
||||
channel,
|
||||
new DoAsUserMessage(
|
||||
|
@@ -9,6 +9,7 @@ using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using NadekoBot.Modules.Gambling;
|
||||
using NadekoBot.Common.TypeReaders;
|
||||
using NadekoBot.Modules.Utility;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
|
||||
|
@@ -18,7 +18,8 @@ public partial class Games
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
public async Task Cleverbot()
|
||||
[NoPublicBot]
|
||||
public async Task CleverBot()
|
||||
{
|
||||
var channel = (ITextChannel)ctx.Channel;
|
||||
|
||||
@@ -30,7 +31,7 @@ public partial class Games
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await Response().Confirm(strs.cleverbot_disabled).SendAsync();
|
||||
await Response().Confirm(strs.chatbot_disabled).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,7 +43,7 @@ public partial class Games
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await Response().Confirm(strs.cleverbot_enabled).SendAsync();
|
||||
await Response().Confirm(strs.chatbot_enabled).SendAsync();
|
||||
}
|
||||
}
|
||||
}
|
@@ -15,43 +15,32 @@ public class ChatterBotService : IExecOnMessage
|
||||
public int Priority
|
||||
=> 1;
|
||||
|
||||
private readonly FeatureLimitKey _flKey;
|
||||
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IPermissionChecker _perms;
|
||||
private readonly CommandHandler _cmd;
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly IPatronageService _ps;
|
||||
private readonly GamesConfigService _gcs;
|
||||
private readonly IMessageSenderService _sender;
|
||||
public readonly IPatronageService _ps;
|
||||
|
||||
public ChatterBotService(
|
||||
DiscordSocketClient client,
|
||||
IPermissionChecker perms,
|
||||
IBot bot,
|
||||
CommandHandler cmd,
|
||||
IPatronageService ps,
|
||||
IHttpClientFactory factory,
|
||||
IBotCredentials creds,
|
||||
IPatronageService ps,
|
||||
GamesConfigService gcs,
|
||||
IMessageSenderService sender)
|
||||
{
|
||||
_client = client;
|
||||
_perms = perms;
|
||||
_cmd = cmd;
|
||||
_creds = creds;
|
||||
_sender = sender;
|
||||
_httpFactory = factory;
|
||||
_ps = ps;
|
||||
_perms = perms;
|
||||
_gcs = gcs;
|
||||
|
||||
_flKey = new FeatureLimitKey()
|
||||
{
|
||||
Key = CleverBotResponseStr.CLEVERBOT_RESPONSE,
|
||||
PrettyName = "Cleverbot Replies"
|
||||
};
|
||||
_ps = ps;
|
||||
|
||||
ChatterBotGuilds = new(bot.AllGuildConfigs
|
||||
.Where(gc => gc.CleverbotEnabled)
|
||||
@@ -69,9 +58,9 @@ public class ChatterBotService : IExecOnMessage
|
||||
|
||||
Log.Information("Cleverbot will not work as the api key is missing");
|
||||
return null;
|
||||
case ChatBotImplementation.Gpt3:
|
||||
case ChatBotImplementation.Gpt:
|
||||
if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey))
|
||||
return new OfficialGpt3Session(_creds.Gpt3ApiKey,
|
||||
return new OfficialGptSession(_creds.Gpt3ApiKey,
|
||||
_gcs.Data.ChatGpt.ModelName,
|
||||
_gcs.Data.ChatGpt.ChatHistory,
|
||||
_gcs.Data.ChatGpt.MaxTokens,
|
||||
@@ -87,19 +76,18 @@ public class ChatterBotService : IExecOnMessage
|
||||
}
|
||||
}
|
||||
|
||||
public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot)
|
||||
public IChatterBotSession GetOrCreateSession(ulong guildId)
|
||||
{
|
||||
var channel = msg.Channel as ITextChannel;
|
||||
cleverbot = null;
|
||||
if (ChatterBotGuilds.TryGetValue(guildId, out var lazyChatBot))
|
||||
return lazyChatBot.Value;
|
||||
|
||||
if (channel is null)
|
||||
return null;
|
||||
|
||||
if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out var lazyCleverbot))
|
||||
return null;
|
||||
|
||||
cleverbot = lazyCleverbot.Value;
|
||||
lazyChatBot = new(() => CreateSession(), true);
|
||||
ChatterBotGuilds.TryAdd(guildId, lazyChatBot);
|
||||
return lazyChatBot.Value;
|
||||
}
|
||||
|
||||
public string PrepareMessage(IUserMessage msg)
|
||||
{
|
||||
var nadekoId = _client.CurrentUser.Id;
|
||||
var normalMention = $"<@{nadekoId}> ";
|
||||
var nickMention = $"<@!{nadekoId}> ";
|
||||
@@ -119,13 +107,31 @@ public class ChatterBotService : IExecOnMessage
|
||||
if (guild is not SocketGuild sg)
|
||||
return false;
|
||||
|
||||
var channel = usrMsg.Channel as ITextChannel;
|
||||
if (channel is null)
|
||||
return false;
|
||||
|
||||
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 message = PrepareMessage(usrMsg, out var cbs);
|
||||
if (message is null || cbs is null)
|
||||
return false;
|
||||
|
||||
var res = await _perms.CheckPermsAsync(sg,
|
||||
var res = await _perms.CheckPermsAsync(guild,
|
||||
usrMsg.Channel,
|
||||
usrMsg.Author,
|
||||
CleverBotResponseStr.CLEVERBOT_RESPONSE,
|
||||
@@ -134,59 +140,33 @@ public class ChatterBotService : IExecOnMessage
|
||||
if (!res.IsAllowed)
|
||||
return false;
|
||||
|
||||
var channel = (ITextChannel)usrMsg.Channel;
|
||||
var conf = _ps.GetConfig();
|
||||
if (!_creds.IsOwner(sg.OwnerId) && conf.IsEnabled)
|
||||
if (!await _ps.LimitHitAsync(LimitedFeatureName.ChatBot, usrMsg.Author.Id, 2048 / 2))
|
||||
{
|
||||
var quota = await _ps.TryGetFeatureLimitAsync(_flKey, sg.OwnerId, 0);
|
||||
|
||||
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;
|
||||
}
|
||||
// limit exceeded
|
||||
return false;
|
||||
}
|
||||
|
||||
_ = channel.TriggerTypingAsync();
|
||||
var response = await cbs.Think(message, usrMsg.Author.ToString());
|
||||
await _sender.Response(channel)
|
||||
.Confirm(response)
|
||||
.SendAsync();
|
||||
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)
|
||||
.Confirm(result.Text)
|
||||
.SendAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("Error in chatterbot: {Error}", error.Value);
|
||||
}
|
||||
|
||||
Log.Information("""
|
||||
CleverBot Executed
|
@@ -3,10 +3,25 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class Gpt3Response
|
||||
public class OpenAiCompletionResponse
|
||||
{
|
||||
[JsonPropertyName("choices")]
|
||||
public Choice[] Choices { get; set; }
|
||||
|
||||
[JsonPropertyName("usage")]
|
||||
public OpenAiUsageData Usage { get; set; }
|
||||
}
|
||||
|
||||
public class OpenAiUsageData
|
||||
{
|
||||
[JsonPropertyName("prompt_tokens")]
|
||||
public int PromptTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("completion_tokens")]
|
||||
public int CompletionTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("total_tokens")]
|
||||
public int TotalTokens { get; set; }
|
||||
}
|
||||
|
||||
public class Choice
|
||||
|
@@ -1,7 +1,10 @@
|
||||
#nullable disable
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public interface IChatterBotSession
|
||||
{
|
||||
Task<string> Think(string input, string username);
|
||||
Task<OneOf<ThinkResult, Error<string>>> Think(string input, string username);
|
||||
}
|
@@ -1,5 +1,7 @@
|
||||
#nullable disable
|
||||
using Newtonsoft.Json;
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
@@ -18,7 +20,7 @@ public class OfficialCleverbotSession : IChatterBotSession
|
||||
_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();
|
||||
var dataString = await http.GetStringAsync(string.Format(QueryString, input, cs ?? ""));
|
||||
@@ -27,12 +29,17 @@ public class OfficialCleverbotSession : IChatterBotSession
|
||||
var data = JsonConvert.DeserializeObject<CleverbotResponse>(dataString);
|
||||
|
||||
cs = data?.Cs;
|
||||
return data?.Output;
|
||||
return new ThinkResult
|
||||
{
|
||||
Text = data?.Output,
|
||||
TokensIn = 2,
|
||||
TokensOut = 1
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
Log.Warning("Unexpected cleverbot response received: {ResponseString}", dataString);
|
||||
return null;
|
||||
Log.Warning("Unexpected response from CleverBot: {ResponseString}", dataString);
|
||||
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,143 @@
|
||||
#nullable disable
|
||||
using Newtonsoft.Json;
|
||||
using OneOf.Types;
|
||||
using System.Net.Http.Json;
|
||||
using SharpToken;
|
||||
|
||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
|
||||
|
||||
public class OfficialGptSession : 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 OfficialGptSession(
|
||||
string apiKey,
|
||||
ChatGptModel model,
|
||||
int chatHistory,
|
||||
int maxTokens,
|
||||
int minTokens,
|
||||
string personality,
|
||||
string nadekoUsername,
|
||||
IHttpClientFactory factory)
|
||||
{
|
||||
_apiKey = apiKey;
|
||||
_httpFactory = factory;
|
||||
|
||||
_model = model switch
|
||||
{
|
||||
ChatGptModel.Gpt35Turbo => "gpt-3.5-turbo",
|
||||
ChatGptModel.Gpt4o => "gpt-4o",
|
||||
_ => throw new ArgumentException("Unknown, unsupported or obsolete model", nameof(model))
|
||||
};
|
||||
|
||||
_maxHistory = chatHistory;
|
||||
_maxTokens = maxTokens;
|
||||
_minTokens = minTokens;
|
||||
_nadekoUsername = nadekoUsername;
|
||||
_encoding = GptEncoding.GetEncodingForModel(_model);
|
||||
messages.Add(new()
|
||||
{
|
||||
Role = "system",
|
||||
Content = personality,
|
||||
Name = _nadekoUsername
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<OneOf.OneOf<ThinkResult, Error<string>>> Think(string input, string 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(Uri,
|
||||
new Gpt3ApiRequest()
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
{
|
||||
[Comment("DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 3;
|
||||
public int Version { get; set; } = 4;
|
||||
|
||||
[Comment("Hangman related settings (.hangman command)")]
|
||||
public HangmanConfig Hangman { get; set; } = new()
|
||||
@@ -105,8 +105,8 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
|
||||
|
||||
[Comment(@"Which chatbot API should bot use.
|
||||
'cleverbot' - bot will use Cleverbot API.
|
||||
'gpt3' - bot will use GPT-3 API")]
|
||||
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt3;
|
||||
'gpt' - bot will use GPT API")]
|
||||
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt;
|
||||
|
||||
public ChatGptConfig ChatGpt { get; set; } = new();
|
||||
}
|
||||
@@ -114,10 +114,10 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
|
||||
[Cloneable]
|
||||
public sealed partial class ChatGptConfig
|
||||
{
|
||||
[Comment(@"Which GPT-3 Model should bot use.
|
||||
[Comment(@"Which GPT Model should bot use.
|
||||
gpt35turbo - cheapest
|
||||
gpt4 - 30x more expensive, higher quality
|
||||
gp432k - same model as above, but with a 32k token limit")]
|
||||
gpt4o - more expensive, higher quality
|
||||
")]
|
||||
public ChatGptModel ModelName { get; set; } = ChatGptModel.Gpt35Turbo;
|
||||
|
||||
[Comment(@"How should the chat bot behave, what's its personality? (Usage of this counts towards the max tokens)")]
|
||||
@@ -126,10 +126,10 @@ public sealed partial class ChatGptConfig
|
||||
[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;
|
||||
|
||||
[Comment(@"The maximum number of tokens to use per GPT-3 API call")]
|
||||
[Comment(@"The maximum number of tokens to use per GPT API call")]
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -163,12 +163,18 @@ public sealed partial class RaceAnimal
|
||||
public enum ChatBotImplementation
|
||||
{
|
||||
Cleverbot,
|
||||
Gpt3
|
||||
Gpt = 1,
|
||||
[Obsolete]
|
||||
Gpt3 = 1,
|
||||
}
|
||||
|
||||
public enum ChatGptModel
|
||||
{
|
||||
Gpt35Turbo,
|
||||
[Obsolete]
|
||||
Gpt4,
|
||||
Gpt432k
|
||||
[Obsolete]
|
||||
Gpt432k,
|
||||
|
||||
Gpt35Turbo,
|
||||
Gpt4o,
|
||||
}
|
@@ -73,15 +73,6 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
|
||||
});
|
||||
}
|
||||
|
||||
if (data.Version < 2)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 2;
|
||||
c.ChatBot = ChatBotImplementation.Cleverbot;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.Version < 3)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
@@ -90,5 +81,19 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
|
||||
c.ChatGpt.ModelName = ChatGptModel.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 == ChatGptModel.Gpt4 || c.ChatGpt.ModelName == ChatGptModel.Gpt432k
|
||||
? ChatGptModel.Gpt4o
|
||||
: c.ChatGpt.ModelName;
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Modules.Music.Services;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Utility;
|
||||
|
||||
namespace NadekoBot.Modules.Music;
|
||||
|
||||
|
@@ -212,7 +212,7 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider
|
||||
if (settings.AutoDisconnect)
|
||||
return LeaveVoiceChannelAsync(guildId);
|
||||
}
|
||||
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
|
@@ -32,5 +32,14 @@ public class PatronageConfig : ConfigServiceBase<PatronConfigData>
|
||||
c.IsEnabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
if (c.Version == 2)
|
||||
{
|
||||
c.Version = 3;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
#nullable disable
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using NadekoBot.Modules.Patronage;
|
||||
using NadekoBot.Services.Currency;
|
||||
@@ -8,7 +9,7 @@ using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Utility;
|
||||
|
||||
public sealed class CurrencyRewardService : INService, IDisposable
|
||||
public sealed class CurrencyRewardService : INService, IReadyExecutor
|
||||
{
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly IPatronageService _ps;
|
||||
@@ -32,16 +33,14 @@ public sealed class CurrencyRewardService : INService, IDisposable
|
||||
_config = config;
|
||||
_client = client;
|
||||
|
||||
}
|
||||
|
||||
public Task OnReadyAsync()
|
||||
{
|
||||
_ps.OnNewPatronPayment += OnNewPayment;
|
||||
_ps.OnPatronRefunded += OnPatronRefund;
|
||||
_ps.OnPatronUpdated += OnPatronUpdate;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_ps.OnNewPatronPayment -= OnNewPayment;
|
||||
_ps.OnPatronRefunded -= OnPatronRefund;
|
||||
_ps.OnPatronUpdated -= OnPatronUpdate;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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,
|
||||
LastChargeStatus = m.Attributes.LastChargeStatus
|
||||
})
|
||||
.Where(x => x.UserId == 140788173885276160)
|
||||
.ToArray();
|
||||
|
||||
yield return userData;
|
||||
|
@@ -25,9 +25,4 @@ public sealed class PatreonMemberData : ISubscriberData
|
||||
"Declined" or "Pending" => SubscriptionChargeStatus.Unpaid,
|
||||
_ => SubscriptionChargeStatus.Other,
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class PatreonPledgeData
|
||||
{
|
||||
|
||||
}
|
@@ -71,17 +71,16 @@ public partial class Help
|
||||
return;
|
||||
}
|
||||
|
||||
var patron = await _service.GetPatronAsync(user.Id);
|
||||
var quotaStats = await _service.GetUserQuotaStatistic(user.Id);
|
||||
var maybePatron = await _service.GetPatronAsync(user.Id);
|
||||
|
||||
var quotaStats = await _service.LimitStats(user.Id);
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
.WithAuthor(user)
|
||||
.WithTitle(GetText(strs.patron_info))
|
||||
.WithOkColor();
|
||||
|
||||
if (quotaStats.Commands.Count == 0
|
||||
&& quotaStats.Groups.Count == 0
|
||||
&& quotaStats.Modules.Count == 0)
|
||||
if (quotaStats.Count == 0 || maybePatron is not { } patron)
|
||||
{
|
||||
eb.WithDescription(GetText(strs.no_quota_found));
|
||||
}
|
||||
@@ -97,26 +96,9 @@ public partial class Help
|
||||
|
||||
eb.AddField(GetText(strs.quotas), "", false);
|
||||
|
||||
if (quotaStats.Commands.Count > 0)
|
||||
{
|
||||
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))
|
||||
eb.AddField(GetText(strs.modules), text, true);
|
||||
}
|
||||
var text = GetQuotaList(quotaStats);
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
eb.AddField(GetText(strs.modules), text, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
foreach (var (key, q) in featureQuotaStats)
|
||||
foreach (var (key, (cur, quota)) in featureQuotaStats)
|
||||
{
|
||||
text += $"\n\t`{key}`\n";
|
||||
if (q.Hourly != default)
|
||||
text += $" {GetEmoji(q.Hourly)} {q.Hourly.Cur}/{q.Hourly.Max} per hour\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";
|
||||
if (quota.QuotaPeriod == QuotaPer.PerHour)
|
||||
text += $" {cur}/{(quota.Quota == -1 ? "∞" : quota.Quota)} {QuotaPeriodToString(quota.QuotaPeriod)}\n";
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private string GetEmoji((uint Cur, uint Max) limit)
|
||||
=> limit.Cur < limit.Max
|
||||
? "✅"
|
||||
: "⚠️";
|
||||
public string QuotaPeriodToString(QuotaPer per)
|
||||
=> 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 NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Db.Models;
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
using CommandInfo = Discord.Commands.CommandInfo;
|
||||
using StackExchange.Redis;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace NadekoBot.Modules.Patronage;
|
||||
|
||||
@@ -12,7 +11,6 @@ namespace NadekoBot.Modules.Patronage;
|
||||
public sealed class PatronageService
|
||||
: IPatronageService,
|
||||
IReadyExecutor,
|
||||
IExecPreCommand,
|
||||
INService
|
||||
{
|
||||
public event Func<Patron, Task> OnNewPatronPayment = static delegate { return Task.CompletedTask; };
|
||||
@@ -60,7 +58,7 @@ public sealed class PatronageService
|
||||
if (_client.ShardId != 0)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return Task.WhenAll(ResetLoopAsync(), LoadSubscribersLoopAsync());
|
||||
return Task.WhenAll(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)
|
||||
{
|
||||
// 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 it's not, just add 1 month to the last charge date
|
||||
var count = await ctx.GetTable<PatronUser>()
|
||||
.Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId)
|
||||
.Where(x => x.UniquePlatformUserId
|
||||
== subscriber.UniquePlatformUserId)
|
||||
.UpdateAsync(old => new()
|
||||
{
|
||||
UserId = subscriber.UserId,
|
||||
@@ -215,14 +149,13 @@ public sealed class PatronageService
|
||||
: 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));
|
||||
}
|
||||
@@ -284,313 +217,7 @@ public sealed class PatronageService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ExecPreCommandAsync(
|
||||
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)
|
||||
public async Task<Patron?> GetPatronAsync(ulong userId)
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
|
||||
@@ -616,128 +243,135 @@ public sealed class PatronageService
|
||||
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)
|
||||
return new();
|
||||
if (!_pConf.Data.IsEnabled)
|
||||
return true;
|
||||
|
||||
var patron = await GetPatronAsync(userId);
|
||||
var userLimit = await GetUserLimit(key, userId);
|
||||
|
||||
await using var ctx = _db.GetDbContext();
|
||||
var allPatronQuotas = await ctx.GetTable<PatronQuota>()
|
||||
.Where(x => x.UserId == userId)
|
||||
.ToListAsync();
|
||||
if (userLimit.Quota == 0)
|
||||
return false;
|
||||
|
||||
var allQuotasDict = allPatronQuotas
|
||||
.GroupBy(static x => x.FeatureType)
|
||||
.ToDictionary(static x => x.Key, static x => x.ToDictionary(static y => y.Feature));
|
||||
if (userLimit.Quota == -1)
|
||||
return true;
|
||||
|
||||
allQuotasDict.TryGetValue(FeatureType.Command, out var data);
|
||||
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,
|
||||
};
|
||||
return await TryAddLimit(key, userLimit, userId, amount);
|
||||
}
|
||||
|
||||
private IReadOnlyDictionary<string, FeatureQuotaStats> GetFeatureQuotaStats(
|
||||
PatronTier patronTier,
|
||||
IReadOnlyDictionary<string, PatronQuota>? allQuotasDict,
|
||||
Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> commands)
|
||||
public async Task<bool> LimitForceHit(LimitedFeatureName key, ulong userId, int amount)
|
||||
{
|
||||
var userCommandQuotaStats = new Dictionary<string, FeatureQuotaStats>();
|
||||
foreach (var (key, quotaData) in commands)
|
||||
if (_creds.GetCreds().IsOwner(userId))
|
||||
return true;
|
||||
|
||||
if (!_pConf.Data.IsEnabled)
|
||||
return true;
|
||||
|
||||
var userLimit = await GetUserLimit(key, userId);
|
||||
|
||||
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)
|
||||
{
|
||||
var cacheKey = CreateKey(key, userId);
|
||||
var cur = await _cache.GetOrAddAsync(cacheKey, () => Task.FromResult(0), GetExpiry(userLimit));
|
||||
|
||||
if (cur + amount < userLimit.Quota)
|
||||
{
|
||||
if (TryGetTierDataOrLower(quotaData, patronTier, out var data))
|
||||
await _cache.AddAsync(cacheKey, cur + amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private TypedKey<int> CreateKey(LimitedFeatureName key, ulong userId)
|
||||
=> new($"limited_feature:{key}:{userId}");
|
||||
|
||||
private readonly QuotaLimit _emptyQuota = new QuotaLimit()
|
||||
{
|
||||
Quota = 0,
|
||||
QuotaPeriod = QuotaPer.PerDay,
|
||||
};
|
||||
|
||||
private readonly QuotaLimit _infiniteQuota = new QuotaLimit()
|
||||
{
|
||||
Quota = -1,
|
||||
QuotaPeriod = QuotaPer.PerDay,
|
||||
};
|
||||
|
||||
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 data is null that means the quota for the user's tier is unlimited
|
||||
// no point in returning it?
|
||||
|
||||
if (data is null)
|
||||
continue;
|
||||
|
||||
var (daily, hourly, monthly) = default((uint, uint, uint));
|
||||
// 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()
|
||||
if (value.TryGetValue(name, out var quotaLimit))
|
||||
{
|
||||
Hourly = data.TryGetValue(QuotaPer.PerHour, out var hourD)
|
||||
? (hourly, hourD)
|
||||
: default,
|
||||
Daily = data.TryGetValue(QuotaPer.PerDay, out var maxD)
|
||||
? (daily, maxD)
|
||||
: default,
|
||||
Monthly = data.TryGetValue(QuotaPer.PerMonth, out var maxM)
|
||||
? (monthly, maxM)
|
||||
: default,
|
||||
};
|
||||
return quotaLimit;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return userCommandQuotaStats;
|
||||
return _emptyQuota;
|
||||
}
|
||||
|
||||
public async Task<FeatureLimit> TryGetFeatureLimitAsync(FeatureLimitKey key, ulong userId, int? defaultValue)
|
||||
public async Task<Dictionary<LimitedFeatureName, (int, QuotaLimit)>> LimitStats(ulong userId)
|
||||
{
|
||||
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,
|
||||
IsPatronLimit = true,
|
||||
};
|
||||
|
||||
return new()
|
||||
var dict = new Dictionary<LimitedFeatureName, (int, QuotaLimit)>();
|
||||
foreach (var featureName in Enum.GetValues<LimitedFeatureName>())
|
||||
{
|
||||
Name = key.PrettyName,
|
||||
Quota = limit,
|
||||
IsPatronLimit = true
|
||||
};
|
||||
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)
|
||||
=> 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)
|
||||
{
|
||||
try
|
||||
@@ -776,28 +426,28 @@ public sealed class PatronageService
|
||||
return;
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle("❤️ Thank you for supporting NadekoBot! ❤️")
|
||||
.WithDescription(
|
||||
"Your donation has been processed and you will receive the rewards shortly.\n"
|
||||
+ "You can visit <https://www.patreon.com/join/nadekobot> to see rewards for your tier. 🎉")
|
||||
.AddField("Tier", Format.Bold(patron.Tier.ToString()), true)
|
||||
.AddField("Pledge", $"**{patron.Amount / 100.0f:N1}$**", true)
|
||||
.AddField("Expires",
|
||||
patron.ValidThru.AddDays(1).ToShortAndRelativeTimestampTag(),
|
||||
true)
|
||||
.AddField("Instructions",
|
||||
"""
|
||||
*- Within the next **1-2 minutes** you will have all of the benefits of the Tier you've subscribed to.*
|
||||
*- You can check your benefits on <https://www.patreon.com/join/nadekobot>*
|
||||
*- You can use the `.patron` command in this chat to check your current quota usage for the Patron-only commands*
|
||||
*- **ALL** of the servers that you **own** will enjoy your Patron benefits.*
|
||||
*- You can use any of the commands available in your tier on any server (assuming you have sufficient permissions to run those commands)*
|
||||
*- Any user in any of your servers can use Patron-only commands, but they will spend **your quota**, which is why it's recommended to use Nadeko's command cooldown system (.h .cmdcd) or permission system to limit the command usage for your server members.*
|
||||
*- Permission guide can be found here if you're not familiar with it: <https://nadekobot.readthedocs.io/en/latest/permissions-system/>*
|
||||
""",
|
||||
inline: false)
|
||||
.WithFooter($"platform id: {patron.UniquePlatformUserId}");
|
||||
.WithOkColor()
|
||||
.WithTitle("❤️ Thank you for supporting NadekoBot! ❤️")
|
||||
.WithDescription(
|
||||
"Your donation has been processed and you will receive the rewards shortly.\n"
|
||||
+ "You can visit <https://www.patreon.com/join/nadekobot> to see rewards for your tier. 🎉")
|
||||
.AddField("Tier", Format.Bold(patron.Tier.ToString()), true)
|
||||
.AddField("Pledge", $"**{patron.Amount / 100.0f:N1}$**", true)
|
||||
.AddField("Expires",
|
||||
patron.ValidThru.AddDays(1).ToShortAndRelativeTimestampTag(),
|
||||
true)
|
||||
.AddField("Instructions",
|
||||
"""
|
||||
*- Within the next **1-2 minutes** you will have all of the benefits of the Tier you've subscribed to.*
|
||||
*- You can check your benefits on <https://www.patreon.com/join/nadekobot>*
|
||||
*- You can use the `.patron` command in this chat to check your current quota usage for the Patron-only commands*
|
||||
*- **ALL** of the servers that you **own** will enjoy your Patron benefits.*
|
||||
*- You can use any of the commands available in your tier on any server (assuming you have sufficient permissions to run those commands)*
|
||||
*- Any user in any of your servers can use Patron-only commands, but they will spend **your quota**, which is why it's recommended to use Nadeko's command cooldown system (.h .cmdcd) or permission system to limit the command usage for your server members.*
|
||||
*- Permission guide can be found here if you're not familiar with it: <https://nadekobot.readthedocs.io/en/latest/permissions-system/>*
|
||||
""",
|
||||
inline: false)
|
||||
.WithFooter($"platform id: {patron.UniquePlatformUserId}");
|
||||
|
||||
await _sender.Response(user).Embed(eb).SendAsync();
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
using AngleSharp;
|
||||
using CsvHelper;
|
||||
using CsvHelper.Configuration;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -22,46 +23,32 @@ public sealed class DefaultStockDataService : IStockDataService, INService
|
||||
|
||||
using var http = _httpClientFactory.CreateClient();
|
||||
|
||||
|
||||
|
||||
var quoteHtmlPage = $"https://finance.yahoo.com/quote/{query.ToUpperInvariant()}";
|
||||
|
||||
var config = Configuration.Default.WithDefaultLoader();
|
||||
using var document = await BrowsingContext.New(config).OpenAsync(quoteHtmlPage);
|
||||
var divElem =
|
||||
document.QuerySelector(
|
||||
"#quote-header-info > div:nth-child(2) > div > div > h1");
|
||||
var tickerName = (divElem)?.TextContent;
|
||||
|
||||
var tickerName = document.QuerySelector("div.top > .left > .container > h1")
|
||||
?.TextContent;
|
||||
|
||||
if (tickerName is null)
|
||||
return default;
|
||||
|
||||
var marketcap = document
|
||||
.QuerySelectorAll("table")
|
||||
.Skip(1)
|
||||
.First()
|
||||
.QuerySelector("tbody > tr > td:nth-child(2)")
|
||||
.QuerySelector("li > span > fin-streamer[data-field='marketCap']")
|
||||
?.TextContent;
|
||||
|
||||
|
||||
var volume = document.QuerySelector("td[data-test='AVERAGE_VOLUME_3MONTH-value']")
|
||||
var volume = document.QuerySelector("li > span > fin-streamer[data-field='regularMarketVolume']")
|
||||
?.TextContent;
|
||||
|
||||
var close= document.QuerySelector("td[data-test='PREV_CLOSE-value']")
|
||||
?.TextContent ?? "0";
|
||||
|
||||
var price = document
|
||||
.QuerySelector("#quote-header-info")
|
||||
?.QuerySelector("fin-streamer[data-field='regularMarketPrice']")
|
||||
?.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();
|
||||
var close = document.QuerySelector("li > span > fin-streamer[data-field='regularMarketPreviousClose']")
|
||||
?.TextContent
|
||||
?? "0";
|
||||
|
||||
// if (symbol is null)
|
||||
// return default;
|
||||
var price = document.QuerySelector("fin-streamer.livePrice > span")
|
||||
?.TextContent
|
||||
?? "0";
|
||||
|
||||
return new()
|
||||
{
|
||||
|
@@ -1,5 +1,4 @@
|
||||
#nullable disable
|
||||
using CodeHollow.FeedReader;
|
||||
using CodeHollow.FeedReader;
|
||||
using NadekoBot.Modules.Searches.Services;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -17,19 +16,21 @@ public partial class Searches
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[Priority(1)]
|
||||
public Task YtUploadNotif(string url, [Leftover] string message = null)
|
||||
public Task YtUploadNotif(string url, [Leftover] string? message = null)
|
||||
=> YtUploadNotif(url, null, message);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[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);
|
||||
if (!m.Success)
|
||||
return Response().Error(strs.invalid_input).SendAsync();
|
||||
|
||||
channel ??= ctx.Channel as ITextChannel;
|
||||
|
||||
if (!((IGuildUser)ctx.User).GetPermissions(channel).MentionEveryone)
|
||||
message = message?.SanitizeAllMentions();
|
||||
|
||||
@@ -42,7 +43,7 @@ public partial class Searches
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[Priority(0)]
|
||||
public Task Feed(string url, [Leftover] string message = null)
|
||||
public Task Feed(string url, [Leftover] string? message = null)
|
||||
=> Feed(url, null, message);
|
||||
|
||||
|
||||
@@ -50,7 +51,7 @@ public partial class Searches
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.ManageMessages)]
|
||||
[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)
|
||||
|| (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
|
||||
@@ -59,10 +60,11 @@ public partial class Searches
|
||||
return;
|
||||
}
|
||||
|
||||
channel ??= (ITextChannel)ctx.Channel;
|
||||
|
||||
if (!((IGuildUser)ctx.User).GetPermissions(channel).MentionEveryone)
|
||||
message = message?.SanitizeAllMentions();
|
||||
|
||||
channel ??= (ITextChannel)ctx.Channel;
|
||||
try
|
||||
{
|
||||
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()
|
||||
{
|
||||
while (true)
|
||||
@@ -94,24 +103,32 @@ public class FeedsService : INService
|
||||
{
|
||||
var feed = await FeedReader.ReadAsync(rssUrl);
|
||||
|
||||
var items = feed
|
||||
.Items.Select(item => (Item: item,
|
||||
LastUpdate: item.PublishingDate?.ToUniversalTime()
|
||||
?? (item.SpecificItem as AtomFeedItem)?.UpdatedDate?.ToUniversalTime()))
|
||||
.Where(data => data.LastUpdate is not null)
|
||||
.Select(data => (data.Item, LastUpdate: (DateTime)data.LastUpdate))
|
||||
.OrderByDescending(data => data.LastUpdate)
|
||||
.Reverse() // start from the oldest
|
||||
.ToList();
|
||||
var items = new List<(FeedItem Item, DateTime LastUpdate)>();
|
||||
foreach (var item in feed.Items)
|
||||
{
|
||||
var pubDate = GetPubDate(item);
|
||||
|
||||
if (pubDate is null)
|
||||
continue;
|
||||
|
||||
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))
|
||||
{
|
||||
lastFeedUpdate = _lastPosts[kvp.Key] =
|
||||
items.Any() ? items[items.Count - 1].LastUpdate : DateTime.UtcNow;
|
||||
lastFeedUpdate = _lastPosts[kvp.Key] = items[0].LastUpdate;
|
||||
}
|
||||
|
||||
foreach (var (feedItem, itemUpdateDate) in items)
|
||||
for (var index = 1; index <= items.Count; index++)
|
||||
{
|
||||
var (feedItem, itemUpdateDate) = items[^index];
|
||||
if (itemUpdateDate <= lastFeedUpdate)
|
||||
continue;
|
||||
|
||||
@@ -168,27 +185,26 @@ public class FeedsService : INService
|
||||
if (!string.IsNullOrWhiteSpace(feedItem.Description))
|
||||
embed.WithDescription(desc.TrimTo(2048));
|
||||
|
||||
//send the created embed to all subscribed channels
|
||||
var feedSendTasks = kvp.Value
|
||||
.Where(x => x.GuildConfig is not null)
|
||||
.Select(x =>
|
||||
{
|
||||
var ch = _client.GetGuild(x.GuildConfig.GuildId)
|
||||
?.GetTextChannel(x.ChannelId);
|
||||
|
||||
if (ch is null)
|
||||
return null;
|
||||
var tasks = new List<Task>();
|
||||
|
||||
return _sender.Response(ch)
|
||||
.Embed(embed)
|
||||
.Text(string.IsNullOrWhiteSpace(x.Message)
|
||||
? string.Empty
|
||||
: x.Message)
|
||||
.SendAsync();
|
||||
})
|
||||
.Where(x => x is not null);
|
||||
foreach (var val in kvp.Value)
|
||||
{
|
||||
var ch = _client.GetGuild(val.GuildConfig.GuildId).GetTextChannel(val.ChannelId);
|
||||
|
||||
allSendTasks.Add(feedSendTasks.WhenAll());
|
||||
if (ch is null)
|
||||
continue;
|
||||
|
||||
var sendTask = _sender.Response(ch)
|
||||
.Embed(embed)
|
||||
.Text(string.IsNullOrWhiteSpace(val.Message)
|
||||
? string.Empty
|
||||
: val.Message)
|
||||
.SendAsync();
|
||||
tasks.Add(sendTask);
|
||||
}
|
||||
|
||||
allSendTasks.Add(tasks.WhenAll());
|
||||
|
||||
// as data retrieval was successful, reset error counter
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -32,20 +32,21 @@ public sealed class SearxSearchService : SearchServiceBase, INService
|
||||
var instanceUrl = GetRandomInstance();
|
||||
|
||||
Log.Information("Using {Instance} instance for web search...", instanceUrl);
|
||||
var sw = Stopwatch.StartNew();
|
||||
var startTime = Stopwatch.GetTimestamp();
|
||||
|
||||
using var http = _http.CreateClient();
|
||||
await using var res = await http.GetStreamAsync($"{instanceUrl}"
|
||||
+ $"?q={Uri.EscapeDataString(query)}"
|
||||
+ $"&format=json"
|
||||
+ $"&strict=2");
|
||||
|
||||
sw.Stop();
|
||||
var elapsed = Stopwatch.GetElapsedTime(startTime);
|
||||
var dat = await JsonSerializer.DeserializeAsync<SearxSearchResult>(res);
|
||||
|
||||
if (dat is null)
|
||||
return new SearxSearchResult();
|
||||
|
||||
dat.SearchTime = sw.Elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
|
||||
dat.SearchTime = elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
|
||||
return dat;
|
||||
}
|
||||
|
||||
@@ -56,7 +57,7 @@ public sealed class SearxSearchService : SearchServiceBase, INService
|
||||
var instanceUrl = GetRandomInstance();
|
||||
|
||||
Log.Information("Using {Instance} instance for img search...", instanceUrl);
|
||||
var sw = Stopwatch.StartNew();
|
||||
var startTime = Stopwatch.GetTimestamp();
|
||||
using var http = _http.CreateClient();
|
||||
await using var res = await http.GetStreamAsync($"{instanceUrl}"
|
||||
+ $"?q={Uri.EscapeDataString(query)}"
|
||||
@@ -64,13 +65,13 @@ public sealed class SearxSearchService : SearchServiceBase, INService
|
||||
+ $"&category_images=on"
|
||||
+ $"&strict=2");
|
||||
|
||||
sw.Stop();
|
||||
var elapsed = Stopwatch.GetElapsedTime(startTime);
|
||||
var dat = await JsonSerializer.DeserializeAsync<SearxImageSearchResult>(res);
|
||||
|
||||
if (dat is null)
|
||||
return new SearxImageSearchResult();
|
||||
|
||||
dat.SearchTime = sw.Elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
|
||||
dat.SearchTime = elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture);
|
||||
return dat;
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NadekoBot.Modules.Searches.Common;
|
||||
using NadekoBot.Modules.Searches.Services;
|
||||
using NadekoBot.Modules.Utility;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SixLabors.ImageSharp;
|
||||
@@ -168,7 +169,7 @@ public partial class Searches : NadekoModule<SearchesService>
|
||||
.AddField("Rating", movie.ImdbRating, true)
|
||||
.AddField("Genre", movie.Genre, true)
|
||||
.AddField("Year", movie.Year, true)
|
||||
.WithImageUrl(movie.Poster))
|
||||
.WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute) ? movie.Poster : null))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
|
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 false;
|
||||
}
|
||||
|
||||
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)
|
||||
.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
|
||||
{
|
||||
|
@@ -113,10 +113,9 @@ public partial class Utility
|
||||
foreach (var rem in rems)
|
||||
{
|
||||
var when = rem.When;
|
||||
var diff = when - DateTime.UtcNow;
|
||||
embed.AddField(
|
||||
$"#{++i + (page * 10)} {rem.When:HH:mm yyyy-MM-dd} UTC "
|
||||
+ $"(in {diff.ToPrettyStringHm()})",
|
||||
+ $"{TimestampTag.FromDateTime(when)}",
|
||||
$@"`Target:` {(rem.IsPrivate ? "DM" : "Channel")}
|
||||
`TargetId:` {rem.ChannelId}
|
||||
`Message:` {rem.Message?.TrimTo(50)}");
|
||||
@@ -203,16 +202,15 @@ public partial class Utility
|
||||
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
|
||||
{
|
||||
await Response()
|
||||
.Confirm($"\u23f0 {GetText(strs.remind(
|
||||
.Confirm($"\u23f0 {GetText(strs.remind2(
|
||||
Format.Bold(!isPrivate ? $"<#{targetId}>" : ctx.User.Username),
|
||||
Format.Bold(message),
|
||||
ts.ToPrettyStringHm(),
|
||||
gTime,
|
||||
gTime))}")
|
||||
TimestampTag.FromDateTime(DateTime.UtcNow.Add(ts), TimestampTagStyles.Relative),
|
||||
TimestampTag.FormatFromDateTime(time, TimestampTagStyles.ShortDateTime)))}")
|
||||
.SendAsync();
|
||||
}
|
||||
catch
|
||||
|
@@ -2,6 +2,7 @@
|
||||
using NadekoBot.Modules.Xp.Services;
|
||||
using NadekoBot.Db.Models;
|
||||
using NadekoBot.Modules.Patronage;
|
||||
using NadekoBot.Modules.Utility;
|
||||
|
||||
namespace NadekoBot.Modules.Xp;
|
||||
|
||||
@@ -479,7 +480,8 @@ public partial class Xp : NadekoModule<XpService>
|
||||
ctx.User.Id,
|
||||
button,
|
||||
OnShopUse,
|
||||
(key, itemType));
|
||||
(key, itemType),
|
||||
clearAfter: false);
|
||||
|
||||
return inter;
|
||||
}
|
||||
@@ -493,7 +495,9 @@ public partial class Xp : NadekoModule<XpService>
|
||||
ctx.User.Id,
|
||||
button,
|
||||
OnShopBuy,
|
||||
(key, itemType));
|
||||
(key, itemType),
|
||||
singleUse: true,
|
||||
clearAfter: false);
|
||||
|
||||
return inter;
|
||||
}
|
||||
@@ -576,6 +580,10 @@ public partial class Xp : NadekoModule<XpService>
|
||||
{
|
||||
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)
|
||||
|
@@ -668,7 +668,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
/// <param name="channel"></param>
|
||||
private async Task ScanUserForVoiceXp(SocketGuildUser user, SocketVoiceChannel channel)
|
||||
{
|
||||
if (UserParticipatingInVoiceChannel(user) && ShouldTrackXp(user, channel.Id))
|
||||
if (UserParticipatingInVoiceChannel(user) && ShouldTrackXp(user, channel))
|
||||
await UserJoinedVoiceChannel(user);
|
||||
else
|
||||
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;
|
||||
|
||||
if (_excludedServers.Contains(user.Guild.Id))
|
||||
@@ -788,7 +791,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
if (!ShouldTrackXp(user, arg.Channel.Id))
|
||||
if (!ShouldTrackXp(user, arg.Channel))
|
||||
return;
|
||||
|
||||
var xpConf = _xpConfig.Data;
|
||||
@@ -1286,9 +1289,9 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
Image? frame = 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"));
|
||||
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"));
|
||||
}
|
||||
else
|
||||
@@ -1465,7 +1468,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>true</ImplicitUsings>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<Version>5.0.7</Version>
|
||||
<Version>5.1.1</Version>
|
||||
|
||||
<!-- Output/build -->
|
||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||
@@ -73,9 +73,9 @@
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.8"/>
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14"/>
|
||||
<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="SharpToken" Version="2.0.2"/>
|
||||
<PackageReference Include="SharpToken" Version="2.0.3" />
|
||||
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0"/>
|
||||
|
||||
|
@@ -133,52 +133,8 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
||||
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()
|
||||
{
|
||||
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))
|
||||
{
|
||||
var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(CREDS_FILE_NAME));
|
||||
@@ -191,6 +147,12 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
||||
creds.Version = 7;
|
||||
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 System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using YamlDotNet.Core.Tokens;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
@@ -47,6 +48,7 @@ public sealed class RedisBotCache : IBotCache
|
||||
var success = await db.StringSetAsync(key.Key,
|
||||
val,
|
||||
expiry: expiry,
|
||||
keepTtl: true,
|
||||
when: overwrite ? When.Always : When.NotExists);
|
||||
|
||||
return success;
|
||||
|
@@ -4,8 +4,9 @@ namespace NadekoBot;
|
||||
public interface IBotCredentials
|
||||
{
|
||||
string Token { get; }
|
||||
string GoogleApiKey { get; }
|
||||
string NadekoAiToken { get; }
|
||||
ICollection<ulong> OwnerIds { get; set; }
|
||||
string GoogleApiKey { get; }
|
||||
bool UsePrivilegedIntents { get; }
|
||||
string RapidApiKey { get; }
|
||||
|
||||
|
@@ -29,6 +29,15 @@ public sealed class Creds : IBotCredentials
|
||||
""")]
|
||||
public int TotalShards { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Pledge 5$ or more on https://patreon.com/nadekobot and connect your discord account to Patreon.
|
||||
Go to https://dashy.nadeko.bot/me and login with your discord account
|
||||
Go to the Keys page and click "Generate New Key" and copy it here
|
||||
You and anyone else with the permission to run `.prompt` command will be able to use natural language to run bot's commands.
|
||||
For example '@Bot how's the weather in Paris' will return the current weather in Paris as if you were to run `.weather Paris` command.
|
||||
""")]
|
||||
public string NadekoAiToken { get; set; }
|
||||
|
||||
[Comment(
|
||||
"""
|
||||
Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
|
||||
@@ -64,7 +73,7 @@ public sealed class Creds : IBotCredentials
|
||||
[Comment("""Official cleverbot api key.""")]
|
||||
public string CleverbotApiKey { get; set; }
|
||||
|
||||
[Comment(@"Official GPT-3 api key.")]
|
||||
[Comment(@"OpenAi api key.")]
|
||||
public string Gpt3ApiKey { get; set; }
|
||||
|
||||
[Comment("""
|
||||
@@ -147,7 +156,7 @@ public sealed class Creds : IBotCredentials
|
||||
|
||||
public Creds()
|
||||
{
|
||||
Version = 7;
|
||||
Version = 9;
|
||||
Token = string.Empty;
|
||||
UsePrivilegedIntents = true;
|
||||
OwnerIds = new List<ulong>();
|
||||
|
@@ -6,14 +6,16 @@ public interface INadekoInteractionService
|
||||
ulong userId,
|
||||
ButtonBuilder button,
|
||||
Func<SocketMessageComponent, Task> onTrigger,
|
||||
bool singleUse = true);
|
||||
bool singleUse = true,
|
||||
bool clearAfter = true);
|
||||
|
||||
public NadekoInteractionBase Create<T>(
|
||||
ulong userId,
|
||||
ButtonBuilder button,
|
||||
Func<SocketMessageComponent, T, Task> onTrigger,
|
||||
in T state,
|
||||
bool singleUse = true);
|
||||
bool singleUse = true,
|
||||
bool clearAfter = true);
|
||||
|
||||
NadekoInteractionBase Create(
|
||||
ulong userId,
|
||||
|
@@ -8,8 +8,9 @@ public sealed class NadekoButtonInteractionHandler : NadekoInteractionBase
|
||||
ButtonBuilder button,
|
||||
Func<SocketMessageComponent, Task> onAction,
|
||||
bool onlyAuthor,
|
||||
bool singleUse = true)
|
||||
: base(client, authorId, button.CustomId, onAction, onlyAuthor, singleUse)
|
||||
bool singleUse = true,
|
||||
bool clearAfter = true)
|
||||
: base(client, authorId, button.CustomId, onAction, onlyAuthor, singleUse, clearAfter)
|
||||
{
|
||||
Button = button;
|
||||
}
|
||||
@@ -18,4 +19,5 @@ public sealed class NadekoButtonInteractionHandler : NadekoInteractionBase
|
||||
|
||||
public override void AddTo(ComponentBuilder cb)
|
||||
=> cb.WithButton(Button);
|
||||
|
||||
}
|
@@ -12,6 +12,7 @@ public abstract class NadekoInteractionBase
|
||||
private IUserMessage message = null!;
|
||||
private readonly string _customId;
|
||||
private readonly bool _singleUse;
|
||||
private readonly bool _clearAfter;
|
||||
|
||||
public NadekoInteractionBase(
|
||||
DiscordSocketClient client,
|
||||
@@ -19,13 +20,16 @@ public abstract class NadekoInteractionBase
|
||||
string customId,
|
||||
Func<SocketMessageComponent, Task> onAction,
|
||||
bool onlyAuthor,
|
||||
bool singleUse = true)
|
||||
bool singleUse = true,
|
||||
bool clearAfter = true)
|
||||
{
|
||||
_authorId = authorId;
|
||||
_customId = customId;
|
||||
_onAction = onAction;
|
||||
_onlyAuthor = onlyAuthor;
|
||||
_singleUse = singleUse;
|
||||
_clearAfter = clearAfter;
|
||||
|
||||
_interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
Client = client;
|
||||
@@ -36,13 +40,11 @@ public abstract class NadekoInteractionBase
|
||||
message = msg;
|
||||
|
||||
Client.InteractionCreated += OnInteraction;
|
||||
if (_singleUse)
|
||||
await Task.WhenAny(Task.Delay(30_000), _interactionCompletedSource.Task);
|
||||
else
|
||||
await Task.Delay(30_000);
|
||||
await Task.WhenAny(Task.Delay(30_000), _interactionCompletedSource.Task);
|
||||
Client.InteractionCreated -= OnInteraction;
|
||||
|
||||
await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
|
||||
if (_clearAfter)
|
||||
await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
|
||||
}
|
||||
|
||||
private Task OnInteraction(SocketInteraction arg)
|
||||
@@ -59,11 +61,15 @@ public abstract class NadekoInteractionBase
|
||||
if (smc.Data.CustomId != _customId)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (_interactionCompletedSource.Task.IsCompleted)
|
||||
return Task.CompletedTask;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_interactionCompletedSource.TrySetResult(true);
|
||||
if (_singleUse)
|
||||
_interactionCompletedSource.TrySetResult(true);
|
||||
await ExecuteOnActionAsync(smc);
|
||||
|
||||
if (!smc.HasResponded)
|
||||
@@ -85,6 +91,9 @@ public abstract class NadekoInteractionBase
|
||||
|
||||
public Task ExecuteOnActionAsync(SocketMessageComponent smc)
|
||||
=> _onAction(smc);
|
||||
|
||||
public void SetCompleted()
|
||||
=> _interactionCompletedSource.TrySetResult(true);
|
||||
}
|
||||
|
||||
public sealed class NadekoModalSubmitHandler
|
||||
|
@@ -13,25 +13,30 @@ public class NadekoInteractionService : INadekoInteractionService, INService
|
||||
ulong userId,
|
||||
ButtonBuilder button,
|
||||
Func<SocketMessageComponent, Task> onTrigger,
|
||||
bool singleUse = true)
|
||||
bool singleUse = true,
|
||||
bool clearAfter = true)
|
||||
=> new NadekoButtonInteractionHandler(_client,
|
||||
userId,
|
||||
button,
|
||||
onTrigger,
|
||||
onlyAuthor: true,
|
||||
singleUse: singleUse);
|
||||
singleUse: singleUse,
|
||||
clearAfter: clearAfter);
|
||||
|
||||
public NadekoInteractionBase Create<T>(
|
||||
ulong userId,
|
||||
ButtonBuilder button,
|
||||
Func<SocketMessageComponent, T, Task> onTrigger,
|
||||
in T state,
|
||||
bool singleUse = true)
|
||||
bool singleUse = true,
|
||||
bool clearAfter = true
|
||||
)
|
||||
=> Create(userId,
|
||||
button,
|
||||
((Func<T, Func<SocketMessageComponent, Task>>)((data)
|
||||
=> smc => onTrigger(smc, data)))(state),
|
||||
singleUse);
|
||||
singleUse,
|
||||
clearAfter);
|
||||
|
||||
public NadekoInteractionBase Create(
|
||||
ulong userId,
|
||||
|
@@ -11,4 +11,4 @@ public class CultureInfoConverter : JsonConverter<CultureInfo>
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, CultureInfo value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.Name);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,30 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
public class NumberToStringConverter : JsonConverter<object>
|
||||
{
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
=> typeof(string) == typeToConvert;
|
||||
|
||||
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonTokenType.Number:
|
||||
return reader.TryGetInt64(out var l)
|
||||
? l.ToString()
|
||||
: reader.GetDouble().ToString(CultureInfo.InvariantCulture);
|
||||
case JsonTokenType.String:
|
||||
return reader.GetString() ?? string.Empty;
|
||||
default:
|
||||
{
|
||||
using var document = JsonDocument.ParseValue(ref reader);
|
||||
return document.RootElement.Clone().ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.ToString());
|
||||
}
|
@@ -1,45 +0,0 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public class OldCreds
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public ulong[] OwnerIds { get; set; } = new ulong[1];
|
||||
public string LoLApiKey { get; set; } = string.Empty;
|
||||
public string GoogleApiKey { get; set; } = string.Empty;
|
||||
public string MashapeKey { get; set; } = string.Empty;
|
||||
public string OsuApiKey { get; set; } = string.Empty;
|
||||
public string CleverbotApiKey { get; set; } = string.Empty;
|
||||
public string CarbonKey { get; set; } = string.Empty;
|
||||
public int TotalShards { get; set; } = 1;
|
||||
public string PatreonAccessToken { get; set; } = string.Empty;
|
||||
public string PatreonCampaignId { get; set; } = "334038";
|
||||
public RestartConfig RestartCommand { get; set; }
|
||||
|
||||
public string ShardRunCommand { get; set; } = string.Empty;
|
||||
public string ShardRunArguments { get; set; } = string.Empty;
|
||||
public int? ShardRunPort { get; set; }
|
||||
public string MiningProxyUrl { get; set; } = string.Empty;
|
||||
public string MiningProxyCreds { get; set; } = string.Empty;
|
||||
|
||||
public string BotListToken { get; set; } = string.Empty;
|
||||
public string TwitchClientId { get; set; } = string.Empty;
|
||||
public string VotesToken { get; set; } = string.Empty;
|
||||
public string VotesUrl { get; set; } = string.Empty;
|
||||
public string RedisOptions { get; set; } = string.Empty;
|
||||
public string LocationIqApiKey { get; set; } = string.Empty;
|
||||
public string TimezoneDbApiKey { get; set; } = string.Empty;
|
||||
public string CoinmarketcapApiKey { get; set; } = string.Empty;
|
||||
|
||||
public class RestartConfig
|
||||
{
|
||||
public string Cmd { get; set; }
|
||||
public string Args { get; set; }
|
||||
|
||||
public RestartConfig(string cmd, string args)
|
||||
{
|
||||
Cmd = cmd;
|
||||
Args = args;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,12 @@
|
||||
namespace NadekoBot.Modules.Patronage;
|
||||
|
||||
public enum LimitedFeatureName
|
||||
{
|
||||
ChatBot,
|
||||
ReactionRole,
|
||||
Prune,
|
||||
|
||||
}
|
||||
public readonly struct FeatureLimitKey
|
||||
{
|
||||
public string PrettyName { get; init; }
|
||||
|
@@ -1,8 +0,0 @@
|
||||
namespace NadekoBot.Modules.Patronage;
|
||||
|
||||
public readonly struct FeatureQuotaStats
|
||||
{
|
||||
public (uint Cur, uint Max) Hourly { get; init; }
|
||||
public (uint Cur, uint Max) Daily { get; init; }
|
||||
public (uint Cur, uint Max) Monthly { get; init; }
|
||||
}
|
@@ -31,26 +31,15 @@ public interface IPatronageService
|
||||
/// </summary>
|
||||
/// <param name="userId">UserId for which to get the patron data for.</param>
|
||||
/// <returns>A patron with the specifeid userId</returns>
|
||||
public Task<Patron> GetPatronAsync(ulong userId);
|
||||
public Task<Patron?> GetPatronAsync(ulong userId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the quota statistic for the user/patron specified by the userId
|
||||
/// </summary>
|
||||
/// <param name="userId">UserId of the user for which to get the quota statistic for</param>
|
||||
/// <returns>Quota stats for the specified user</returns>
|
||||
Task<UserQuotaStats> GetUserQuotaStatistic(ulong userId);
|
||||
|
||||
Task<bool> LimitHitAsync(LimitedFeatureName key, ulong userId, int amount = 1);
|
||||
Task<bool> LimitForceHit(LimitedFeatureName key, ulong userId, int amount);
|
||||
Task<QuotaLimit> GetUserLimit(LimitedFeatureName name, ulong userId);
|
||||
|
||||
Task<FeatureLimit> TryGetFeatureLimitAsync(FeatureLimitKey key, ulong userId, int? defaultValue);
|
||||
|
||||
ValueTask<OneOf<(uint Hourly, uint Daily, uint Monthly), QuotaLimit>> TryIncrementQuotaCounterAsync(
|
||||
ulong userId,
|
||||
bool isSelf,
|
||||
FeatureType featureType,
|
||||
string featureName,
|
||||
uint? maybeHourly,
|
||||
uint? maybeDaily,
|
||||
uint? maybeMonthly);
|
||||
Task<Dictionary<LimitedFeatureName, (int, QuotaLimit)>> LimitStats(ulong userId);
|
||||
|
||||
PatronConfigData GetConfig();
|
||||
int PercentBonus(Patron? user);
|
||||
int PercentBonus(long amount);
|
||||
}
|
@@ -13,7 +13,7 @@ public readonly struct Patron
|
||||
public ulong UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Amount the Patron is currently pledging or paid
|
||||
/// Amount the Patron is currently pledging or paid in cents
|
||||
/// </summary>
|
||||
public int Amount { get; init; }
|
||||
|
||||
|
@@ -7,31 +7,11 @@ namespace NadekoBot.Modules.Patronage;
|
||||
public partial class PatronConfigData : ICloneable<PatronConfigData>
|
||||
{
|
||||
[Comment("DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 2;
|
||||
|
||||
public int Version { get; set; } = 3;
|
||||
|
||||
[Comment("Whether the patronage feature is enabled")]
|
||||
public bool IsEnabled { get; set; }
|
||||
|
||||
[Comment("List of patron only features and relevant quota data")]
|
||||
public FeatureQuotas Quotas { get; set; }
|
||||
|
||||
public PatronConfigData()
|
||||
{
|
||||
Quotas = new();
|
||||
}
|
||||
|
||||
public class FeatureQuotas
|
||||
{
|
||||
[Comment("Dictionary of feature names with their respective limits. Set to null for unlimited")]
|
||||
public Dictionary<string, Dictionary<PatronTier, int?>> Features { get; set; } = new();
|
||||
|
||||
[Comment("Dictionary of commands with their respective quota data")]
|
||||
public Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> Commands { get; set; } = new();
|
||||
|
||||
[Comment("Dictionary of groups with their respective quota data")]
|
||||
public Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> Groups { get; set; } = new();
|
||||
|
||||
[Comment("Dictionary of modules with their respective quota data")]
|
||||
public Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> Modules { get; set; } = new();
|
||||
}
|
||||
[Comment("Who can do how much of what")]
|
||||
public Dictionary<int, Dictionary<LimitedFeatureName, QuotaLimit>> Limits { get; set; } = new();
|
||||
}
|
@@ -8,15 +8,6 @@ public static class PatronExtensions
|
||||
_ => $"Patron Tier {tier}",
|
||||
};
|
||||
|
||||
public static string ToFullName(this QuotaPer per)
|
||||
=> per switch
|
||||
{
|
||||
QuotaPer.PerDay => "per day",
|
||||
QuotaPer.PerHour => "per hour",
|
||||
QuotaPer.PerMonth => "per month",
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
public static DateTime DayOfNextMonth(this DateTime date, int day)
|
||||
{
|
||||
var nextMonth = date.AddMonths(1);
|
||||
|
@@ -10,57 +10,16 @@ public readonly struct QuotaLimit
|
||||
/// <summary>
|
||||
/// Amount of usages reached, which is the limit
|
||||
/// </summary>
|
||||
public uint Quota { get; init; }
|
||||
public int Quota { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Which period is this quota limit for (hourly, daily, monthly, etc...)
|
||||
/// </summary>
|
||||
public QuotaPer QuotaPeriod { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When does this quota limit reset
|
||||
/// </summary>
|
||||
public DateTime ResetsAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the feature this quota limit is for
|
||||
/// </summary>
|
||||
public FeatureType FeatureType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the feature this quota limit is for
|
||||
/// </summary>
|
||||
public string Feature { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether it is the user's own quota (true), or server owners (false)
|
||||
/// </summary>
|
||||
public bool IsOwnQuota { get; init; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Respresent information about the feature limit
|
||||
/// </summary>
|
||||
public readonly struct FeatureLimit
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Whether this limit comes from the patronage system
|
||||
/// </summary>
|
||||
public bool IsPatronLimit { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum limit allowed
|
||||
/// </summary>
|
||||
public int? Quota { get; init; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the limit
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
public FeatureLimit()
|
||||
public QuotaLimit(int quota, QuotaPer quotaPeriod)
|
||||
{
|
||||
Quota = quota;
|
||||
QuotaPeriod = quotaPeriod;
|
||||
}
|
||||
}
|
@@ -5,4 +5,5 @@ public enum QuotaPer
|
||||
PerHour,
|
||||
PerDay,
|
||||
PerMonth,
|
||||
Total,
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
namespace NadekoBot.Modules.Patronage;
|
||||
|
||||
public readonly struct UserQuotaStats
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, FeatureQuotaStats> _emptyDictionary
|
||||
= new Dictionary<string, FeatureQuotaStats>();
|
||||
public PatronTier Tier { get; init; }
|
||||
= PatronTier.None;
|
||||
|
||||
public IReadOnlyDictionary<string, FeatureQuotaStats> Features { get; init; }
|
||||
= _emptyDictionary;
|
||||
|
||||
public IReadOnlyDictionary<string, FeatureQuotaStats> Commands { get; init; }
|
||||
= _emptyDictionary;
|
||||
|
||||
public IReadOnlyDictionary<string, FeatureQuotaStats> Groups { get; init; }
|
||||
= _emptyDictionary;
|
||||
|
||||
public IReadOnlyDictionary<string, FeatureQuotaStats> Modules { get; init; }
|
||||
= _emptyDictionary;
|
||||
|
||||
public UserQuotaStats()
|
||||
{
|
||||
}
|
||||
}
|
@@ -11,6 +11,10 @@ public partial class ResponseBuilder
|
||||
private readonly ResponseBuilder _builder;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private int currentPage;
|
||||
|
||||
private NadekoButtonInteractionHandler? left;
|
||||
private NadekoButtonInteractionHandler? right;
|
||||
private NadekoInteractionBase? extra;
|
||||
|
||||
public PaginationSender(
|
||||
SourcedPaginatedResponseBuilder<T> paginationBuilder,
|
||||
@@ -67,7 +71,8 @@ public partial class ResponseBuilder
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
true,
|
||||
singleUse: false);
|
||||
singleUse: false,
|
||||
clearAfter: false);
|
||||
|
||||
if (_paginationBuilder.InteractionFunc is not null)
|
||||
{
|
||||
@@ -102,10 +107,13 @@ public partial class ResponseBuilder
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
true,
|
||||
singleUse: false);
|
||||
singleUse: false,
|
||||
clearAfter: false);
|
||||
|
||||
return (leftBtnInter, maybeInter, rightBtnInter);
|
||||
}
|
||||
|
||||
(left, extra, right) = await GetInteractions();
|
||||
|
||||
async Task UpdatePageAsync(SocketMessageComponent smc)
|
||||
{
|
||||
@@ -114,21 +122,25 @@ public partial class ResponseBuilder
|
||||
if (_paginationBuilder.AddPaginatedFooter)
|
||||
toSend.AddPaginatedFooter(currentPage, lastPage);
|
||||
|
||||
var (left, extra, right) = (await GetInteractions());
|
||||
left?.SetCompleted();
|
||||
right?.SetCompleted();
|
||||
extra?.SetCompleted();
|
||||
(left, extra, right) = (await GetInteractions());
|
||||
|
||||
var cb = new ComponentBuilder();
|
||||
left.AddTo(cb);
|
||||
right.AddTo(cb);
|
||||
extra?.AddTo(cb);
|
||||
|
||||
|
||||
await smc.ModifyOriginalResponseAsync(x =>
|
||||
{
|
||||
x.Embed = toSend.Build();
|
||||
x.Components = cb.Build();
|
||||
});
|
||||
|
||||
await Task.WhenAll(left.RunAsync(smc.Message), extra?.RunAsync(smc.Message) ?? Task.CompletedTask, right.RunAsync(smc.Message));
|
||||
}
|
||||
|
||||
var (left, extra, right) = await GetInteractions();
|
||||
|
||||
var cb = new ComponentBuilder();
|
||||
left.AddTo(cb);
|
||||
@@ -144,9 +156,11 @@ public partial class ResponseBuilder
|
||||
|
||||
if (lastPage == 0 && _paginationBuilder.InteractionFunc is null)
|
||||
return;
|
||||
|
||||
|
||||
await Task.WhenAll(left.RunAsync(msg), extra?.RunAsync(msg) ?? Task.CompletedTask, right.RunAsync(msg));
|
||||
|
||||
await Task.Delay(30_000);
|
||||
|
||||
await msg.ModifyAsync(mp => mp.Components = new ComponentBuilder().Build());
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ namespace NadekoBot.Services;
|
||||
|
||||
public class CommandHandler : INService, IReadyExecutor, ICommandHandler
|
||||
{
|
||||
private const int GLOBAL_COMMANDS_COOLDOWN = 750;
|
||||
private const int GLOBAL_COMMANDS_COOLDOWN = 200;
|
||||
|
||||
private const float ONE_THOUSANDTH = 1.0f / 1000;
|
||||
|
||||
@@ -262,7 +262,7 @@ public class CommandHandler : INService, IReadyExecutor, ICommandHandler
|
||||
var blockTime = Environment.TickCount - startTime;
|
||||
|
||||
var messageContent = await _behaviorHandler.RunInputTransformersAsync(guild, usrMsg);
|
||||
|
||||
|
||||
var prefix = GetPrefix(guild?.Id);
|
||||
var isPrefixCommand = messageContent.StartsWith(".prefix", StringComparison.InvariantCultureIgnoreCase);
|
||||
// execute the command and measure the time it took
|
||||
|
@@ -53,7 +53,7 @@ public class YtdlOperation
|
||||
}
|
||||
catch (Win32Exception)
|
||||
{
|
||||
Log.Error("youtube-dl is likely not installed. " + "Please install it before running the command again");
|
||||
Log.Error("youtube-dl is likely not installed. Please install it before running the command again");
|
||||
return default;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@@ -27,4 +27,14 @@ public static class CommandContextExtensions
|
||||
|
||||
public static Task WarningAsync(this ICommandContext ctx)
|
||||
=> ctx.ReactAsync(MsgType.Pending);
|
||||
|
||||
|
||||
public static Task OkAsync(this IUserMessage msg)
|
||||
=> msg.AddReactionAsync(_okEmoji);
|
||||
|
||||
public static Task ErrorAsync(this IUserMessage msg)
|
||||
=> msg.AddReactionAsync(_errorEmoji);
|
||||
|
||||
public static Task WarningAsync(this IUserMessage msg)
|
||||
=> msg.AddReactionAsync(_warnEmoji);
|
||||
}
|
@@ -229,9 +229,4 @@ public static class Extensions
|
||||
public static IEnumerable<IRole> GetRoles(this IGuildUser user)
|
||||
=> user.RoleIds.Select(r => user.Guild.GetRole(r)).Where(r => r is not null);
|
||||
|
||||
public static void Lap(this Stopwatch sw, string checkpoint)
|
||||
{
|
||||
Log.Information("Checkpoint {CheckPoint}: {Time}ms", checkpoint, sw.Elapsed.TotalMilliseconds);
|
||||
sw.Restart();
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
# DO NOT CHANGE
|
||||
version: 7
|
||||
version: 9
|
||||
# Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/
|
||||
token: ''
|
||||
# List of Ids of the users who have bot owner permissions
|
||||
@@ -13,6 +13,12 @@ usePrivilegedIntents: true
|
||||
# note: If you are planning to have more than one shard, then you must change botCache to 'redis'.
|
||||
# Also, in that case you should be using NadekoBot.Coordinator to start the bot, and it will correctly override this value.
|
||||
totalShards: 1
|
||||
# Pledge 5$ or more on https://patreon.com/nadekobot and connect your discord account to Patreon.
|
||||
# Go to https://dashy.nadeko.bot/me and login with your discord account
|
||||
# Go to the Keys page and click "Generate New Key" and copy it here
|
||||
# You and anyone else with the permission to run `.prompt` command will be able to use natural language to run bot's commands.
|
||||
# For example '@Bot how's the weather in Paris' will return the current weather in Paris as if you were to run `.weather Paris` command.
|
||||
nadekoAiToken:
|
||||
# Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
|
||||
# Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
|
||||
# Used only for Youtube Data Api (at the moment).
|
||||
@@ -56,7 +62,7 @@ patreon:
|
||||
botListToken: ''
|
||||
# Official cleverbot api key.
|
||||
cleverbotApiKey: ''
|
||||
# Official GPT-3 api key.
|
||||
# OpenAi api key.
|
||||
gpt3ApiKey: ''
|
||||
# Which cache implementation should bot use.
|
||||
# 'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset.
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user