This commit is contained in:
Toastie 2024-06-28 17:05:04 +12:00
commit fb17ad7ad5
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
238 changed files with 12627 additions and 2130 deletions

View file

@ -2,6 +2,40 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## [5.1.0] - 28.06.2024
### Added
- Added `'prompt` command, Ellie Ai Assistant
- You can send natural language questions, queries or execute commands. For example "@Ellie 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 @Ellie bot. If you're selfhosting, you also will need to acquire the api key from <https://dashy.elliebot.net/api> (coming soon(ish)...)
- Added support for `gpt-4o` in `data/games.yml`
- Added EllieAiToken to `creds.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] - 19.06.2024
### Added

View file

@ -53,8 +53,6 @@ public abstract class EllieContext : 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; }
@ -597,16 +595,6 @@ public abstract class EllieContext : 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

View file

@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace EllieBot.Db.Models;
public class AntiRaidSetting : DbEntity
{
public int GuildConfigId { get; set; }

View file

@ -1,41 +1,17 @@
#nullable disable
namespace EllieBot.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; }
public ulong UserId { get; set; }
public int AmountCents { get; set; }
public DateTime LastCharge { get; set; }
// Date Only component
public DateTime ValidThru { get; set; }
public PatronUser Clone()
=> new PatronUser()
{

View file

@ -4,7 +4,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.0.8</Version>
<Version>5.1.0</Version>
<!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
@ -22,88 +22,88 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.1.2">
<PrivateAssets>all</PrivateAssets>
<Publish>True</Publish>
</PackageReference>
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.6" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Discord.Net" Version="3.204.0" />
<PackageReference Include="CoreCLR-NCalc" Version="3.1.246" />
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414" />
<PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084" />
<!-- <PackageReference Include="Grpc.AspNetCore" Version="2.62.0" />-->
<PackageReference Include="Google.Protobuf" Version="3.26.1" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.62.0" />
<PackageReference Include="Grpc.Tools" Version="2.63.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.5.0" />
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.1.2">
<PrivateAssets>all</PrivateAssets>
<Publish>True</Publish>
</PackageReference>
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.6"/>
<PackageReference Include="CommandLineParser" Version="2.9.1"/>
<PackageReference Include="Discord.Net" Version="3.204.0"/>
<PackageReference Include="CoreCLR-NCalc" Version="3.1.246"/>
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138"/>
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414"/>
<PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084"/>
<!-- <PackageReference Include="Grpc.AspNetCore" Version="2.62.0" />-->
<PackageReference Include="Google.Protobuf" Version="3.26.1"/>
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.62.0"/>
<PackageReference Include="Grpc.Tools" Version="2.63.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.5.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0"/>
<PackageReference Include="MorseCode.ITask" Version="2.0.3" />
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0" />
<PackageReference Include="MorseCode.ITask" Version="2.0.3"/>
<PackageReference Include="NetEscapades.Configuration.Yaml" Version="3.1.0"/>
<!-- DI -->
<!-- <PackageReference Include="Ninject" Version="3.3.6"/>-->
<!-- <PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0"/>-->
<!-- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />-->
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
<!-- <PackageReference Include="Scrutor" Version="4.2.0" />-->
<!-- DI -->
<!-- <PackageReference Include="Ninject" Version="3.3.6"/>-->
<!-- <PackageReference Include="Ninject.Extensions.Conventions" Version="3.3.0"/>-->
<!-- <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />-->
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1"/>
<PackageReference Include="DryIoc.dll" Version="5.4.3"/>
<!-- <PackageReference Include="Scrutor" Version="4.2.0" />-->
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NonBlocking" Version="2.1.2" />
<PackageReference Include="OneOf" Version="3.0.263" />
<PackageReference Include="OneOf.SourceGenerator" Version="3.0.263" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0"/>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="NonBlocking" Version="2.1.2"/>
<PackageReference Include="OneOf" Version="3.0.263"/>
<PackageReference Include="OneOf.SourceGenerator" Version="3.0.263"/>
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/>
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1"/>
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta17" />
<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="YamlDotNet" Version="15.1.4" />
<PackageReference Include="SharpToken" Version="2.0.2" />
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta17"/>
<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="YamlDotNet" Version="15.1.4"/>
<PackageReference Include="SharpToken" Version="2.0.3" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0"/>
<!-- Db-related packages -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<!-- Db-related packages -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.4"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0" />
<PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2"/>
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/>
<!-- Used by stream notifications -->
<PackageReference Include="TwitchLib.Api" Version="3.4.1" />
<!-- Used by stream notifications -->
<PackageReference Include="TwitchLib.Api" Version="3.4.1"/>
<!-- sqlselectcsv and stock -->
<PackageReference Include="CsvHelper" Version="32.0.3" />
<!-- sqlselectcsv and stock -->
<PackageReference Include="CsvHelper" Version="32.0.3"/>
</ItemGroup>
</ItemGroup>
<ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ellie.Marmalade\Ellie.Marmalade.csproj" />
<ProjectReference Include="..\EllieBot.Voice\EllieBot.Voice.csproj" />
<ProjectReference Include="..\EllieBot.Generators\EllieBot.Generators.csproj" OutputItemType="Analyzer" />

View file

@ -1418,7 +1418,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1422,7 +1422,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1451,7 +1451,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1451,7 +1451,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1521,7 +1521,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1554,7 +1554,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1558,7 +1558,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1596,7 +1596,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1600,7 +1600,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1588,7 +1588,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1592,7 +1592,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1592,7 +1592,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1621,7 +1621,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1654,7 +1654,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1658,7 +1658,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1666,7 +1666,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1670,7 +1670,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1700,7 +1700,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1670,7 +1670,7 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.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");
}
}
}

View file

@ -1718,41 +1718,6 @@ namespace EllieBot.Migrations.Mysql
b.ToTable("expressions", (string)null);
});
modelBuilder.Entity("EllieBot.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("EllieBot.Db.Models.PatronUser", b =>
{
b.Property<ulong>("UserId")

View file

@ -1490,7 +1490,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1521,7 +1521,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1521,7 +1521,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1591,7 +1591,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1626,7 +1626,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1630,7 +1630,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1670,7 +1670,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1674,7 +1674,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1662,7 +1662,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1666,7 +1666,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1666,7 +1666,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1697,7 +1697,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1732,7 +1732,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1736,7 +1736,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1744,7 +1744,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1748,7 +1748,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1699,7 +1699,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1669,7 +1669,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.NadekoExpression", b =>
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.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");
}
}
}

View file

@ -1717,41 +1717,6 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("expressions", (string)null);
});
modelBuilder.Entity("EllieBot.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("EllieBot.Db.Models.PatronUser", b =>
{
b.Property<decimal>("UserId")

View file

@ -11,6 +11,8 @@ namespace EllieBot.Migrations
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
MigrationQueries.GuildConfigCleanup(migrationBuilder);
migrationBuilder.DropForeignKey(
name: "FK_AntiRaidSetting_GuildConfigs_GuildConfigId",
table: "AntiRaidSetting");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.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");
}
}
}

View file

@ -1279,33 +1279,6 @@ namespace EllieBot.Migrations
b.ToTable("Expressions");
});
modelBuilder.Entity("EllieBot.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("EllieBot.Db.Models.PatronUser", b =>
{
b.Property<ulong>("UserId")

View file

@ -61,17 +61,66 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
}));
}
// 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<EllieExpression>()
.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,

View file

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

View file

@ -0,0 +1,9 @@
#nullable disable
namespace EllieBot.Modules.Administration.Services;
public enum PruneResult
{
Success,
AlreadyRunning,
FeatureLimit,
}

View file

@ -1,4 +1,6 @@
#nullable disable
using EllieBot.Modules.Patronage;
namespace EllieBot.Modules.Administration.Services;
public class PruneService : IEService
@ -7,11 +9,15 @@ public class PruneService : IEService
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 : IEService
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 : IEService
.ToArray();
if (!msgs.Any())
return;
return PruneResult.Success;
lastMessage = msgs[^1];
@ -88,6 +99,8 @@ public class PruneService : IEService
{
_pruningGuilds.TryRemove(channel.GuildId, out _);
}
return PruneResult.Success;
}
public async Task<bool> CancelAsync(ulong guildId)

View file

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

View file

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

View file

@ -21,22 +21,16 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, 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, IEService, 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, IEService, 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()

View file

@ -19,7 +19,7 @@ public sealed class CheckForUpdatesService : IEService, IReadyExecutor
private readonly IMessageSenderService _sender;
private const string RELEASES_URL = "https://toastielab.dev/api/v1/repos/Emotions-stuff/EllieBot/releases";
private const string RELEASES_URL = "https://toastielab.dev/api/v1/repos/Emotions-stuff/elliebot/releases";
public CheckForUpdatesService(
BotConfigService bcs,
@ -72,7 +72,7 @@ public sealed class CheckForUpdatesService : IEService, IReadyExecutor
UpdateLastKnownVersion(latestVersion);
// pull changelog
var changelog = await http.GetStringAsync("https://toastielab.dev/Emotions-stuff/Ellie/raw/branch/v5/CHANGELOG.md");
var changelog = await http.GetStringAsync("https://toastielab.dev/Emotions-stuff/elliebot/raw/branch/v5/CHANGELOG.md");
var thisVersionChangelog = GetVersionChangelog(latestVersion, changelog);
@ -95,7 +95,7 @@ public sealed class CheckForUpdatesService : IEService, IReadyExecutor
.WithOkColor()
.WithAuthor($"EllieBot v{latest} Released!")
.WithTitle("Changelog")
.WithUrl("https://toastielab.dev/Emotions-stuff/Ellie/src/branch/v5/CHANGELOG.md")
.WithUrl("https://toastielab.dev/Emotions-stuff/elliebot/src/branch/v5/CHANGELOG.md")
.WithDescription(thisVersionChangelog.TrimTo(4096))
.WithFooter(
"You may disable these messages by typing '.conf bot checkforupdates false'");

View file

@ -42,7 +42,7 @@ public static class EllieExpressionExtensions
() => canMentionEveryone
? ctx.Content[substringIndex..].Trim()
: ctx.Content[substringIndex..].Trim().SanitizeMentions(true));
var text = SmartText.CreateFrom(cr.Response);
text = await repSvc.ReplaceAsync(text, repCtx);

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
using EllieBot.Db.Models;
namespace EllieBot.Modules.EllieExpressions;

View file

@ -124,11 +124,11 @@ public sealed class EllieExpressionsService : IExecOnMessage, IReadyExecutor
newguildExpressions = guildItems.GroupBy(k => k.GuildId!.Value)
.ToDictionary(g => g.Key,
g => g.Select(x =>
{
x.Trigger = x.Trigger.Replace(MENTION_PH,
_client.CurrentUser.Mention);
return x;
})
{
x.Trigger = x.Trigger.Replace(MENTION_PH,
_client.CurrentUser.Mention);
return x;
})
.ToArray())
.ToConcurrent();
@ -259,7 +259,7 @@ public sealed class EllieExpressionsService : IExecOnMessage, IReadyExecutor
"ACTUALEXPRESSIONS",
expr.Trigger
);
if (!result.IsAllowed)
{
var cache = _pc.GetCacheFor(guild.Id);

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
using EllieBot.Db.Models;
namespace EllieBot.Modules.EllieExpressions;

View file

@ -3,6 +3,7 @@ using EllieBot.Common.TypeReaders;
using EllieBot.Modules.Gambling.Common;
using EllieBot.Modules.Gambling.Common.Blackjack;
using EllieBot.Modules.Gambling.Services;
using EllieBot.Modules.Utility;
namespace EllieBot.Modules.Gambling;

View file

@ -1,7 +1,6 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Db;
using EllieBot.Db.Models;
using EllieBot.Modules.Gambling.Bank;
using EllieBot.Modules.Gambling.Common;
@ -14,6 +13,7 @@ using System.Text;
using EllieBot.Modules.Gambling.Rps;
using EllieBot.Common.TypeReaders;
using EllieBot.Modules.Patronage;
using EllieBot.Modules.Utility;
namespace EllieBot.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 EllieInteractionBase 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 EllieInteractionBase 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)
{

View file

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

View file

@ -9,6 +9,7 @@ using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using EllieBot.Modules.Gambling;
using EllieBot.Common.TypeReaders;
using EllieBot.Modules.Utility;
using Color = SixLabors.ImageSharp.Color;
using Image = SixLabors.ImageSharp.Image;

View file

@ -5,7 +5,7 @@ public class WaifuInfo : DbEntity
{
public int WaifuId { get; set; }
public DiscordUser Waifu { get; set; }
public int? ClaimerId { get; set; }
public DiscordUser Claimer { get; set; }

View file

@ -66,12 +66,12 @@ public static class WaifuExtensions
await ctx.Set<WaifuInfo>()
.ToLinqToDBTable()
.InsertOrUpdateAsync(() => new()
{
AffinityId = null,
ClaimerId = null,
Price = 1,
WaifuId = ctx.Set<DiscordUser>().Where(x => x.UserId == userId).Select(x => x.Id).First()
},
{
AffinityId = null,
ClaimerId = null,
Price = 1,
WaifuId = ctx.Set<DiscordUser>().Where(x => x.UserId == userId).Select(x => x.Id).First()
},
_ => new(),
() => new()
{

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
namespace EllieBot.Db.Models;
public class WaifuItem : DbEntity

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
namespace EllieBot.Db.Models;
public class WaifuUpdate : DbEntity

View file

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

View file

@ -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,22 +76,21 @@ 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;
lazyChatBot = new(() => CreateSession(), true);
ChatterBotGuilds.TryAdd(guildId, lazyChatBot);
return lazyChatBot.Value;
}
if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out var lazyCleverbot))
return null;
cleverbot = lazyCleverbot.Value;
var ellieId = _client.CurrentUser.Id;
var normalMention = $"<@{ellieId}> ";
var nickMention = $"<@!{ellieId}> ";
public string PrepareMessage(IUserMessage msg)
{
var nadekoId = _client.CurrentUser.Id;
var normalMention = $"<@{nadekoId}> ";
var nickMention = $"<@!{nadekoId}> ";
string message;
if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture))
message = msg.Content[normalMention.Length..].Trim();
@ -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/elliebot) 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);
}
Log.Information("""
CleverBot Executed

View file

@ -3,10 +3,25 @@ using System.Text.Json.Serialization;
namespace EllieBot.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

View file

@ -1,7 +1,10 @@
#nullable disable
using OneOf;
using OneOf.Types;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public interface IChatterBotSession
{
Task<string> Think(string input, string username);
Task<OneOf<ThinkResult, Error<string>>> Think(string input, string username);
}

View file

@ -1,5 +1,7 @@
#nullable disable
using Newtonsoft.Json;
using OneOf;
using OneOf.Types;
namespace EllieBot.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");
}
}
}

View file

@ -1,105 +0,0 @@
#nullable disable
using Newtonsoft.Json;
using System.Net.Http.Json;
using SharpToken;
namespace EllieBot.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 _ellieUsername;
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 ellieUsername,
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;
_ellieUsername = ellieUsername;
_encoding = GptEncoding.GetEncodingForModel(_model);
messages.Add(new GPTMessage(){Role = "user", Content = personality, Name = _ellieUsername});
}
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 = _ellieUsername});
return message;
}
catch
{
Log.Warning("Unexpected GPT-3 response received: {ResponseString}", dataString);
return null;
}
}
}

View file

@ -0,0 +1,141 @@
#nullable disable
using Newtonsoft.Json;
using OneOf.Types;
using System.Net.Http.Json;
using SharpToken;
namespace EllieBot.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);
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; }
}

View file

@ -8,7 +8,7 @@ namespace EllieBot.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,
}

View file

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

View file

@ -524,7 +524,7 @@ public sealed partial class Help : EllieModule<HelpService>
=> smc.RespondConfirmAsync(_sender,
"""
- In case you don't want or cannot Donate to EllieBot project, but you
- EllieBot is a free and [open source](https://toastielab.dev/Emotions-stuff/Ellie) project which means you can run your own "selfhosted" instance on your computer.
- EllieBot is a free and [open source](https://toastielab.dev/Emotions-stuff/elliebot) project which means you can run your own "selfhosted" instance on your computer.
*Keep in mind that running the bot on your computer means that the bot will be offline when you turn off your computer*

View file

@ -1,6 +1,7 @@
#nullable disable
using EllieBot.Modules.Music.Services;
using EllieBot.Db.Models;
using EllieBot.Modules.Utility;
namespace EllieBot.Modules.Music;

View file

@ -212,7 +212,7 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider
if (settings.AutoDisconnect)
return LeaveVoiceChannelAsync(guildId);
}
return Task.CompletedTask;
};

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
namespace EllieBot.Db.Models;
public class MusicPlaylist : DbEntity

View file

@ -4,10 +4,11 @@ namespace EllieBot.Modules.Patronage;
public class PatronageConfig : ConfigServiceBase<PatronConfigData>
{
public override string Name
public override string Name
=> "patron";
private static readonly TypedKey<PatronConfigData> _changeKey;
private static readonly TypedKey<PatronConfigData> _changeKey
= new("config.patron.updated");
private const string FILE_PATH = "data/patron.yml";
@ -31,5 +32,14 @@ public class PatronageConfig : ConfigServiceBase<PatronConfigData>
c.IsEnabled = false;
}
});
ModifyConfig(c =>
{
if (c.Version == 2)
{
c.Version = 3;
}
});
}
}

View file

@ -1,6 +1,7 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Modules.Gambling.Services;
using EllieBot.Modules.Patronage;
using EllieBot.Services.Currency;
@ -8,7 +9,7 @@ using EllieBot.Db.Models;
namespace EllieBot.Modules.Utility;
public sealed class CurrencyRewardService : IEService, IDisposable
public sealed class CurrencyRewardService : IEService, IReadyExecutor
{
private readonly ICurrencyService _cs;
private readonly IPatronageService _ps;
@ -32,16 +33,14 @@ public sealed class CurrencyRewardService : IEService, 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)
@ -58,17 +57,17 @@ public sealed class CurrencyRewardService : IEService, IDisposable
old = await ctx.GetTable<RewardedUser>()
.Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId)
.FirstOrDefaultAsync();
if (old is null)
{
await OnNewPayment(newPatron);
return;
}
// no action as the amount is the same or lower
if (old.AmountRewardedThisMonth >= newAmount)
return;
var count = await ctx.GetTable<RewardedUser>()
.Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId)
.UpdateAsync(_ => new()
@ -91,21 +90,21 @@ public sealed class CurrencyRewardService : IEService, IDisposable
(int)(newAmount / conf.PatreonCurrencyPerCent),
newAmount,
out var percentBonus);
var realOldAmount = GetRealCurrencyReward(
(int)(oldAmount / conf.PatreonCurrencyPerCent),
oldAmount,
out _);
var diff = realNewAmount - realOldAmount;
if (diff <= 0)
return; // no action if new is lower
// if the user pledges 5$ or more, they will get X % more flowers where X is amount in dollars,
// up to 100%
await _cs.AddAsync(newPatron.UserId, diff, new TxData("patron", "update"));
await _cs.AddAsync(newPatron.UserId, diff, new TxData("patron","update"));
_ = SendMessageToUser(newPatron.UserId,
$"You've received an additional **{diff}**{_config.Data.Currency.Sign} as a currency reward (+{percentBonus}%)!");
}
@ -140,12 +139,12 @@ public sealed class CurrencyRewardService : IEService, IDisposable
await using var ctx = _db.GetDbContext();
await ctx.GetTable<RewardedUser>()
.InsertOrUpdateAsync(() => new()
{
PlatformUserId = patron.UniquePlatformUserId,
UserId = patron.UserId,
AmountRewardedThisMonth = amount,
LastReward = patron.PaidAt,
},
{
PlatformUserId = patron.UniquePlatformUserId,
UserId = patron.UserId,
AmountRewardedThisMonth = amount,
LastReward = patron.PaidAt,
},
old => new()
{
AmountRewardedThisMonth = amount,
@ -156,7 +155,7 @@ public sealed class CurrencyRewardService : IEService, IDisposable
{
PlatformUserId = patron.UniquePlatformUserId
});
var realAmount = GetRealCurrencyReward(patron.Amount, amount, out var percentBonus);
await _cs.AddAsync(patron.UserId, realAmount, new("patron", "new"));
_ = SendMessageToUser(patron.UserId,
@ -174,7 +173,7 @@ public sealed class CurrencyRewardService : IEService, IDisposable
var eb = _sender.CreateEmbed()
.WithOkColor()
.WithDescription(message);
await _sender.Response(user).Embed(eb).SendAsync();
}
catch

View file

@ -1,11 +0,0 @@
using EllieBot.Db.Models;
namespace EllieBot.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; }
}

View file

@ -11,11 +11,11 @@ public class PatreonClient : IDisposable
private readonly string _clientId;
private readonly string _clientSecret;
private string refreshToken;
private string accessToken = string.Empty;
private readonly HttpClient _http;
private DateTime refreshAt = DateTime.UtcNow;
public PatreonClient(string clientId, string clientSecret, string refreshToken)
@ -101,7 +101,7 @@ public class PatreonClient : IDisposable
return OneOf<IAsyncEnumerable<IReadOnlyCollection<PatreonMemberData>>, Error<string>>.FromT0(
GetMembersInternalAsync(campaignId));
}
private async IAsyncEnumerable<IReadOnlyCollection<PatreonMemberData>> GetMembersInternalAsync(string campaignId)
{
_http.DefaultRequestHeaders.Clear();
@ -140,9 +140,8 @@ public class PatreonClient : IDisposable
LastChargeDate = m.Attributes.LastChargeDate,
LastChargeStatus = m.Attributes.LastChargeStatus
})
.Where(x => x.UserId == 140788173885276160)
.ToArray();
yield return userData;
} while (!string.IsNullOrWhiteSpace(page = data.Links?.Next));

View file

@ -7,4 +7,4 @@ public readonly struct PatreonCredentials
public string ClientSecret { get; init; }
public string AccessToken { get; init; }
public string RefreshToken { get; init; }
}
}

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Patronage;

View file

@ -25,9 +25,4 @@ public sealed class PatreonMemberData : ISubscriberData
"Declined" or "Pending" => SubscriptionChargeStatus.Unpaid,
_ => SubscriptionChargeStatus.Other,
};
}
public sealed class PatreonPledgeData
{
}

View file

@ -6,8 +6,8 @@ namespace EllieBot.Modules.Patronage;
/// </summary>
public sealed class PatreonSubscriptionHandler : ISubscriptionHandler, IEService
{
private readonly IBotCredsProvider _credsProvider;
private readonly PatreonClient _patreonClient;
private readonly IBotCredsProvider _credsProvider;
private readonly PatreonClient _patreonClient;
public PatreonSubscriptionHandler(IBotCredsProvider credsProvider)
{
@ -15,26 +15,26 @@ public sealed class PatreonSubscriptionHandler : ISubscriptionHandler, IEService
var botCreds = credsProvider.GetCreds();
_patreonClient = new PatreonClient(botCreds.Patreon.ClientId, botCreds.Patreon.ClientSecret, botCreds.Patreon.RefreshToken);
}
public async IAsyncEnumerable<IReadOnlyCollection<ISubscriberData>> GetPatronsAsync()
{
var botCreds = _credsProvider.GetCreds();
if (string.IsNullOrWhiteSpace(botCreds.Patreon.CampaignId)
|| string.IsNullOrWhiteSpace(botCreds.Patreon.ClientId)
|| string.IsNullOrWhiteSpace(botCreds.Patreon.ClientSecret)
|| string.IsNullOrWhiteSpace(botCreds.Patreon.RefreshToken))
yield break;
var result = await _patreonClient.RefreshTokenAsync(false);
if (!result.TryPickT0(out _, out var error))
{
Log.Warning("Unable to refresh patreon token: {ErrorMessage}", error.Value);
yield break;
}
var patreonCreds = _patreonClient.GetCredentials();
_credsProvider.ModifyCredsFile(c =>
{
c.Patreon.AccessToken = patreonCreds.AccessToken;
@ -58,7 +58,7 @@ public sealed class PatreonSubscriptionHandler : ISubscriptionHandler, IEService
Log.Warning(ex,
"Unexpected error while refreshing patreon members: {ErroMessage}",
ex.Message);
yield break;
}
@ -71,7 +71,7 @@ public sealed class PatreonSubscriptionHandler : ISubscriptionHandler, IEService
&& x.LastCharge is { } lc
&& lc.ToUniversalTime().ToDateOnly() >= firstOfThisMonth)
.ToArray();
if (toReturn.Length > 0)
yield return toReturn;
}

View file

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

View file

@ -2,9 +2,8 @@
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using OneOf;
using OneOf.Types;
using CommandInfo = Discord.Commands.CommandInfo;
using StackExchange.Redis;
using System.Diagnostics;
namespace EllieBot.Modules.Patronage;
@ -12,7 +11,6 @@ namespace EllieBot.Modules.Patronage;
public sealed class PatronageService
: IPatronageService,
IReadyExecutor,
IExecPreCommand,
IEService
{
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/elliebot)",
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 EllieBot! ❤️")
.WithDescription(
"Your donation has been processed and you will receive the rewards shortly.\n"
+ "You can visit <https://www.patreon.com/join/elliebot> 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/elliebot>*
*- 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 Ellie'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://docs.elliebot.net/ellie/placeholders/>*
""",
inline: false)
.WithFooter($"platform id: {patron.UniquePlatformUserId}");
.WithOkColor()
.WithTitle("❤️ Thank you for supporting EllieBot! ❤️")
.WithDescription(
"Your donation has been processed and you will receive the rewards shortly.\n"
+ "You can visit <https://www.patreon.com/join/elliebot> 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/elliebot>*
*- 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 Ellie'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://docs.elliebot.net/ellie/features/permissions-system/>*
""",
inline: false)
.WithFooter($"platform id: {patron.UniquePlatformUserId}");
await _sender.Response(user).Embed(eb).SendAsync();
}

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
using EllieBot.Modules.Permissions.Services;
using EllieBot.Db.Models;
@ -46,7 +46,7 @@ public partial class Permissions
Log.Warning("Can't get {BlacklistType} [{BlacklistItemId}]",
i.Type,
i.ItemId);
return Task.FromResult(Format.Code(i.ItemId.ToString()));
}
})

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