Compare commits

..

No commits in common. "fb17ad7ad57a07e0a9cbcd82da2f7cdda664612e" and "ae42d6ce37ff1a5110e5af040245e146d5ea11c6" have entirely different histories.

238 changed files with 2126 additions and 12623 deletions

View file

@ -2,40 +2,6 @@
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,6 +53,8 @@ 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; }
@ -595,6 +597,16 @@ 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,6 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema;
namespace EllieBot.Db.Models;
public class AntiRaidSetting : DbEntity
{
public int GuildConfigId { get; set; }

View file

@ -1,6 +1,30 @@
#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; }

View file

@ -4,7 +4,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.1.0</Version>
<Version>5.0.8</Version>
<!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
@ -27,79 +27,79 @@
<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="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="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.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="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.3" />
<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="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" 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"/>
<PackageReference Include="TwitchLib.Api" Version="3.4.1" />
<!-- sqlselectcsv and stock -->
<PackageReference Include="CsvHelper" Version="32.0.3"/>
<PackageReference Include="CsvHelper" Version="32.0.3" />
</ItemGroup>

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,44 +0,0 @@
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,6 +1718,41 @@ 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Services.Database.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Db.Models.NadekoExpression", 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.EllieExpression", b =>
modelBuilder.Entity("EllieBot.Db.Models.NadekoExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()

View file

@ -1,42 +0,0 @@
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,6 +1717,41 @@ 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,8 +11,6 @@ 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

@ -1,42 +0,0 @@
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,6 +1279,33 @@ 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,66 +61,17 @@ 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,43 +45,23 @@ public partial class Administration
var progressMsg = await Response().Pending(strs.prune_progress(0, 100)).SendAsync();
var progress = GetProgressTracker(progressMsg);
PruneResult result;
if (opts.Safe)
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
await _service.PruneWhere((ITextChannel)ctx.Channel,
100,
x => x.Author.Id == user.Id && !x.IsPinned,
progress,
opts.After);
else
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
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)]
@ -103,21 +83,19 @@ public partial class Administration
var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync();
var progress = GetProgressTracker(progressMsg);
PruneResult result;
if (opts.Safe)
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
await _service.PruneWhere((ITextChannel)ctx.Channel,
count,
x => !x.IsPinned && x.Id != progressMsg.Id,
progress,
opts.After);
else
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
await _service.PruneWhere((ITextChannel)ctx.Channel,
count,
x => x.Id != progressMsg.Id,
progress,
opts.After);
await SendResult(result);
await progressMsg.DeleteAsync();
}
@ -177,10 +155,9 @@ public partial class Administration
var progressMsg = await Response().Pending(strs.prune_progress(0, count)).SendAsync();
var progress = GetProgressTracker(progressMsg);
PruneResult result;
if (opts.Safe)
{
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
await _service.PruneWhere((ITextChannel)ctx.Channel,
count,
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned,
progress,
@ -189,7 +166,7 @@ public partial class Administration
}
else
{
result = await _service.PruneWhere((ITextChannel)ctx.Channel,
await _service.PruneWhere((ITextChannel)ctx.Channel,
count,
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks,
progress,
@ -197,7 +174,6 @@ public partial class Administration
);
}
await SendResult(result);
await progressMsg.DeleteAsync();
}

View file

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

View file

@ -1,6 +1,4 @@
#nullable disable
using EllieBot.Modules.Patronage;
namespace EllieBot.Modules.Administration.Services;
public class PruneService : IEService
@ -9,15 +7,11 @@ 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, IPatronageService ps)
{
_logService = logService;
_ps = ps;
}
public PruneService(ILogCommandService logService)
=> _logService = logService;
public async Task<PruneResult> PruneWhere(
public async Task PruneWhere(
ITextChannel channel,
int amount,
Func<IMessage, bool> predicate,
@ -32,12 +26,7 @@ public class PruneService : IEService
using var cancelSource = new CancellationTokenSource();
if (!_pruningGuilds.TryAdd(channel.GuildId, cancelSource))
return PruneResult.AlreadyRunning;
if (!await _ps.LimitHitAsync(LimitedFeatureName.Prune, channel.Guild.OwnerId))
{
return PruneResult.FeatureLimit;
}
return;
try
{
@ -58,7 +47,7 @@ public class PruneService : IEService
.ToArray();
if (!msgs.Any())
return PruneResult.Success;
return;
lastMessage = msgs[^1];
@ -99,8 +88,6 @@ 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, Error>> AddReactionRole(
Task<OneOf<Success, FeatureLimit>> AddReactionRole(
IGuild guild,
IMessage msg,
string emote,

View file

@ -55,10 +55,12 @@ public partial class Administration
await res.Match(
_ => ctx.OkAsync(),
async fl =>
fl =>
{
_ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
return !fl.IsPatronLimit
? Response().Error(strs.limit_reached(fl.Quota)).SendAsync()
: Response().Pending(strs.feature_limit_reached_owner(fl.Quota, fl.Name)).SendAsync();
});
}

View file

@ -21,16 +21,22 @@ 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)
IBotCredentials creds,
IPatronageService ps)
{
_db = db;
_ps = ps;
_client = client;
_creds = creds;
_ps = ps;
_cache = new();
}
@ -236,7 +242,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, Error>> AddReactionRole(
public async Task<OneOf<Success, FeatureLimit>> AddReactionRole(
IGuild guild,
IMessage msg,
string emote,
@ -255,12 +261,9 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
.Where(x => x.GuildId == guild.Id)
.CountAsync();
var limit = await _ps.GetUserLimit(LimitedFeatureName.ReactionRole, guild.OwnerId);
if (!_creds.IsOwner(guild.OwnerId) && (activeReactionRoles >= limit.Quota && limit.Quota >= 0))
{
return new Error();
}
var result = await _ps.TryGetFeatureLimitAsync(_reroFLKey, guild.OwnerId, 50);
if (result.Quota != -1 && activeReactionRoles >= result.Quota)
return result;
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/elliebot/raw/branch/v5/CHANGELOG.md");
var changelog = await http.GetStringAsync("https://toastielab.dev/Emotions-stuff/Ellie/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/elliebot/src/branch/v5/CHANGELOG.md")
.WithUrl("https://toastielab.dev/Emotions-stuff/Ellie/src/branch/v5/CHANGELOG.md")
.WithDescription(thisVersionChangelog.TrimTo(4096))
.WithFooter(
"You may disable these messages by typing '.conf bot checkforupdates false'");

View file

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

View file

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

View file

@ -3,7 +3,6 @@ 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,6 +1,7 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Db;
using EllieBot.Db.Models;
using EllieBot.Modules.Gambling.Bank;
using EllieBot.Modules.Gambling.Common;
@ -13,7 +14,6 @@ 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,6 +133,12 @@ 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);
@ -148,7 +154,6 @@ 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,
@ -159,17 +164,6 @@ public partial class Gambling : GamblingModule<GamblingService>
(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()
{
@ -181,31 +175,25 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } remainder)
{
// Get correct time form remainder
var interaction = CreateRemindMeInteraction(remainder.TotalMilliseconds);
var inter = CreateRemindMeInteraction(period);
if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } rem)
{
// Removes timely button if there is a timely reminder in DB
if (_service.UserHasTimelyReminder(ctx.User.Id))
{
interaction = null;
inter = null;
}
var now = DateTime.UtcNow;
var relativeTag = TimestampTag.FromDateTime(now.Add(remainder), TimestampTagStyles.Relative);
await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(interaction).SendAsync();
var relativeTag = TimestampTag.FromDateTime(now.Add(rem), TimestampTagStyles.Relative);
await Response().Pending(strs.timely_already_claimed(relativeTag)).Interaction(inter).SendAsync();
return;
}
var result = await _ps.TryGetFeatureLimitAsync(_timelyKey, ctx.User.Id, 0);
var patron = await _ps.GetPatronAsync(ctx.User.Id);
var percentBonus = (_ps.PercentBonus(patron) / 100f);
val += (int)(val * percentBonus);
var inter = CreateRemindMeInteraction(period);
val = (int)(val * (1 + (result.Quota! * 0.01f)));
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
@ -904,7 +892,6 @@ 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,14 +247,7 @@ public partial class Gambling
}
else
{
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 cmd = entry.Command.Replace("%you%", ctx.User.Id.ToString());
var eb = _sender.CreateEmbed()
.WithPendingColor()
.WithTitle("Executing shop command")
@ -266,7 +259,6 @@ 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,7 +9,6 @@ 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

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

@ -15,32 +15,43 @@ 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,
IPatronageService ps,
CommandHandler cmd,
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;
_ps = ps;
_flKey = new FeatureLimitKey()
{
Key = CleverBotResponseStr.CLEVERBOT_RESPONSE,
PrettyName = "Cleverbot Replies"
};
ChatterBotGuilds = new(bot.AllGuildConfigs
.Where(gc => gc.CleverbotEnabled)
@ -58,9 +69,9 @@ public class ChatterBotService : IExecOnMessage
Log.Information("Cleverbot will not work as the api key is missing");
return null;
case ChatBotImplementation.Gpt:
case ChatBotImplementation.Gpt3:
if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey))
return new OfficialGptSession(_creds.Gpt3ApiKey,
return new OfficialGpt3Session(_creds.Gpt3ApiKey,
_gcs.Data.ChatGpt.ModelName,
_gcs.Data.ChatGpt.ChatHistory,
_gcs.Data.ChatGpt.MaxTokens,
@ -76,21 +87,22 @@ public class ChatterBotService : IExecOnMessage
}
}
public IChatterBotSession GetOrCreateSession(ulong guildId)
public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot)
{
if (ChatterBotGuilds.TryGetValue(guildId, out var lazyChatBot))
return lazyChatBot.Value;
var channel = msg.Channel as ITextChannel;
cleverbot = null;
lazyChatBot = new(() => CreateSession(), true);
ChatterBotGuilds.TryAdd(guildId, lazyChatBot);
return lazyChatBot.Value;
}
if (channel is null)
return null;
public string PrepareMessage(IUserMessage msg)
{
var nadekoId = _client.CurrentUser.Id;
var normalMention = $"<@{nadekoId}> ";
var nickMention = $"<@!{nadekoId}> ";
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}> ";
string message;
if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture))
message = msg.Content[normalMention.Length..].Trim();
@ -107,31 +119,13 @@ 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 res = await _perms.CheckPermsAsync(guild,
var message = PrepareMessage(usrMsg, out var cbs);
if (message is null || cbs is null)
return false;
var res = await _perms.CheckPermsAsync(sg,
usrMsg.Channel,
usrMsg.Author,
CleverBotResponseStr.CLEVERBOT_RESPONSE,
@ -140,33 +134,59 @@ public class ChatterBotService : IExecOnMessage
if (!res.IsAllowed)
return false;
if (!await _ps.LimitHitAsync(LimitedFeatureName.ChatBot, usrMsg.Author.Id, 2048 / 2))
var channel = (ITextChannel)usrMsg.Channel;
var conf = _ps.GetConfig();
if (!_creds.IsOwner(sg.OwnerId) && conf.IsEnabled)
{
// limit exceeded
return false;
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;
}
}
_ = channel.TriggerTypingAsync();
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));
var response = await cbs.Think(message, usrMsg.Author.ToString());
await _sender.Response(channel)
.Confirm(result.Text)
.Confirm(response)
.SendAsync();
}
else
{
Log.Warning("Error in chatterbot: {Error}", error);
}
Log.Information("""
CleverBot Executed

View file

@ -18,8 +18,7 @@ public partial class Games
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[NoPublicBot]
public async Task CleverBot()
public async Task Cleverbot()
{
var channel = (ITextChannel)ctx.Channel;
@ -31,7 +30,7 @@ public partial class Games
await uow.SaveChangesAsync();
}
await Response().Confirm(strs.chatbot_disabled).SendAsync();
await Response().Confirm(strs.cleverbot_disabled).SendAsync();
return;
}
@ -43,7 +42,7 @@ public partial class Games
await uow.SaveChangesAsync();
}
await Response().Confirm(strs.chatbot_enabled).SendAsync();
await Response().Confirm(strs.cleverbot_enabled).SendAsync();
}
}
}

View file

@ -3,25 +3,10 @@ using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OpenAiCompletionResponse
public class Gpt3Response
{
[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,10 +1,7 @@
#nullable disable
using OneOf;
using OneOf.Types;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public interface IChatterBotSession
{
Task<OneOf<ThinkResult, Error<string>>> Think(string input, string username);
Task<string> Think(string input, string username);
}

View file

@ -1,7 +1,5 @@
#nullable disable
using Newtonsoft.Json;
using OneOf;
using OneOf.Types;
namespace EllieBot.Modules.Games.Common.ChatterBot;
@ -20,7 +18,7 @@ public class OfficialCleverbotSession : IChatterBotSession
_httpFactory = factory;
}
public async Task<OneOf<ThinkResult, Error<string>>> Think(string input, string username)
public async Task<string> Think(string input, string username)
{
using var http = _httpFactory.CreateClient();
var dataString = await http.GetStringAsync(string.Format(QueryString, input, cs ?? ""));
@ -29,17 +27,12 @@ public class OfficialCleverbotSession : IChatterBotSession
var data = JsonConvert.DeserializeObject<CleverbotResponse>(dataString);
cs = data?.Cs;
return new ThinkResult
{
Text = data?.Output,
TokensIn = 2,
TokensOut = 1
};
return data?.Output;
}
catch
{
Log.Warning("Unexpected response from CleverBot: {ResponseString}", dataString);
return new Error<string>("Unexpected CleverBot response received");
Log.Warning("Unexpected cleverbot response received: {ResponseString}", dataString);
return null;
}
}
}

View file

@ -0,0 +1,105 @@
#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

@ -1,141 +0,0 @@
#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; } = 4;
public int Version { get; set; } = 3;
[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.
'gpt' - bot will use GPT API")]
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt;
'gpt3' - bot will use GPT-3 API")]
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt3;
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 Model should bot use.
[Comment(@"Which GPT-3 Model should bot use.
gpt35turbo - cheapest
gpt4o - more expensive, higher quality
")]
gpt4 - 30x more expensive, higher quality
gp432k - same model as above, but with a 32k token limit")]
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 API call")]
[Comment(@"The maximum number of tokens to use per GPT-3 API call")]
public int MaxTokens { get; set; } = 100;
[Comment(@"The minimum number of tokens to use per GPT API call, such that chat history is removed to make room.")]
[Comment(@"The minimum number of tokens to use per GPT-3 API call, such that chat history is removed to make room.")]
public int MinTokens { get; set; } = 30;
}
@ -163,18 +163,12 @@ public sealed partial class RaceAnimal
public enum ChatBotImplementation
{
Cleverbot,
Gpt = 1,
[Obsolete]
Gpt3 = 1,
Gpt3
}
public enum ChatGptModel
{
[Obsolete]
Gpt4,
[Obsolete]
Gpt432k,
Gpt35Turbo,
Gpt4o,
Gpt4,
Gpt432k
}

View file

@ -73,6 +73,15 @@ 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 =>
@ -81,19 +90,5 @@ 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/elliebot) 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/Ellie) 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,7 +1,6 @@
#nullable disable
using EllieBot.Modules.Music.Services;
using EllieBot.Db.Models;
using EllieBot.Modules.Utility;
namespace EllieBot.Modules.Music;

View file

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

View file

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

View file

@ -1,7 +1,6 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Modules.Gambling.Services;
using EllieBot.Modules.Patronage;
using EllieBot.Services.Currency;
@ -9,7 +8,7 @@ using EllieBot.Db.Models;
namespace EllieBot.Modules.Utility;
public sealed class CurrencyRewardService : IEService, IReadyExecutor
public sealed class CurrencyRewardService : IEService, IDisposable
{
private readonly ICurrencyService _cs;
private readonly IPatronageService _ps;
@ -33,14 +32,16 @@ public sealed class CurrencyRewardService : IEService, IReadyExecutor
_config = config;
_client = client;
}
public Task OnReadyAsync()
{
_ps.OnNewPatronPayment += OnNewPayment;
_ps.OnPatronRefunded += OnPatronRefund;
_ps.OnPatronUpdated += OnPatronUpdate;
return Task.CompletedTask;
}
public void Dispose()
{
_ps.OnNewPatronPayment -= OnNewPayment;
_ps.OnPatronRefunded -= OnPatronRefund;
_ps.OnPatronUpdated -= OnPatronUpdate;
}
private async Task OnPatronUpdate(Patron oldPatron, Patron newPatron)
@ -103,7 +104,7 @@ public sealed class CurrencyRewardService : IEService, IReadyExecutor
// 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}%)!");

View file

@ -0,0 +1,11 @@
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

@ -140,6 +140,7 @@ public class PatreonClient : IDisposable
LastChargeDate = m.Attributes.LastChargeDate,
LastChargeStatus = m.Attributes.LastChargeStatus
})
.Where(x => x.UserId == 140788173885276160)
.ToArray();
yield return userData;

View file

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

View file

@ -26,3 +26,8 @@ public sealed class PatreonMemberData : ISubscriberData
_ => SubscriptionChargeStatus.Other,
};
}
public sealed class PatreonPledgeData
{
}

View file

@ -71,16 +71,17 @@ public partial class Help
return;
}
var maybePatron = await _service.GetPatronAsync(user.Id);
var quotaStats = await _service.LimitStats(user.Id);
var patron = await _service.GetPatronAsync(user.Id);
var quotaStats = await _service.GetUserQuotaStatistic(user.Id);
var eb = _sender.CreateEmbed()
.WithAuthor(user)
.WithTitle(GetText(strs.patron_info))
.WithOkColor();
if (quotaStats.Count == 0 || maybePatron is not { } patron)
if (quotaStats.Commands.Count == 0
&& quotaStats.Groups.Count == 0
&& quotaStats.Modules.Count == 0)
{
eb.WithDescription(GetText(strs.no_quota_found));
}
@ -96,10 +97,27 @@ public partial class Help
eb.AddField(GetText(strs.quotas), "", false);
var text = GetQuotaList(quotaStats);
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);
}
}
try
@ -113,28 +131,26 @@ public partial class Help
}
}
private string GetQuotaList(
IReadOnlyDictionary<LimitedFeatureName, (int Cur, QuotaLimit Quota)> featureQuotaStats)
private string GetQuotaList(IReadOnlyDictionary<string, FeatureQuotaStats> featureQuotaStats)
{
var text = string.Empty;
foreach (var (key, (cur, quota)) in featureQuotaStats)
foreach (var (key, q) in featureQuotaStats)
{
text += $"\n\t`{key}`\n";
if (quota.QuotaPeriod == QuotaPer.PerHour)
text += $" {cur}/{(quota.Quota == -1 ? "" : quota.Quota)} {QuotaPeriodToString(quota.QuotaPeriod)}\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";
}
return text;
}
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)
};
private string GetEmoji((uint Cur, uint Max) limit)
=> limit.Cur < limit.Max
? "✅"
: "⚠️";
}
}

View file

@ -2,8 +2,9 @@
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using StackExchange.Redis;
using System.Diagnostics;
using OneOf;
using OneOf.Types;
using CommandInfo = Discord.Commands.CommandInfo;
namespace EllieBot.Modules.Patronage;
@ -11,6 +12,7 @@ namespace EllieBot.Modules.Patronage;
public sealed class PatronageService
: IPatronageService,
IReadyExecutor,
IExecPreCommand,
IEService
{
public event Func<Patron, Task> OnNewPatronPayment = static delegate { return Task.CompletedTask; };
@ -58,7 +60,7 @@ public sealed class PatronageService
if (_client.ShardId != 0)
return Task.CompletedTask;
return Task.WhenAll(LoadSubscribersLoopAsync());
return Task.WhenAll(ResetLoopAsync(), LoadSubscribersLoopAsync());
}
private async Task LoadSubscribersLoopAsync()
@ -83,6 +85,71 @@ 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
@ -136,8 +203,7 @@ 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,
@ -149,13 +215,14 @@ public sealed class PatronageService
: dateInOneMonth,
});
// this should never happen
if (count == 0)
{
// await tran.RollbackAsync();
continue;
}
dbPatron.UserId = subscriber.UserId;
dbPatron.AmountCents = subscriber.Cents;
dbPatron.LastCharge = lastChargeUtc;
dbPatron.ValidThru = dbPatron.ValidThru >= todayDate
? dbPatron.ValidThru.AddMonths(1)
: dateInOneMonth;
// await tran.CommitAsync();
await OnNewPatronPayment(PatronUserToPatron(dbPatron));
}
@ -217,7 +284,313 @@ public sealed class PatronageService
}
}
public async Task<Patron?> GetPatronAsync(ulong userId)
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)
{
await using var ctx = _db.GetDbContext();
@ -243,135 +616,128 @@ public sealed class PatronageService
return PatronUserToPatron(max);
}
public async Task<bool> LimitHitAsync(LimitedFeatureName key, ulong userId, int amount = 1)
public async Task<UserQuotaStats> GetUserQuotaStatistic(ulong userId)
{
if (_creds.GetCreds().IsOwner(userId))
return true;
var pConfData = _pConf.Data;
if (!_pConf.Data.IsEnabled)
return true;
if (!pConfData.IsEnabled)
return new();
var userLimit = await GetUserLimit(key, userId);
var patron = await GetPatronAsync(userId);
if (userLimit.Quota == 0)
return false;
await using var ctx = _db.GetDbContext();
var allPatronQuotas = await ctx.GetTable<PatronQuota>()
.Where(x => x.UserId == userId)
.ToListAsync();
if (userLimit.Quota == -1)
return true;
var allQuotasDict = allPatronQuotas
.GroupBy(static x => x.FeatureType)
.ToDictionary(static x => x.Key, static x => x.ToDictionary(static y => y.Feature));
return await TryAddLimit(key, userLimit, userId, amount);
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,
};
}
public async Task<bool> LimitForceHit(LimitedFeatureName key, ulong userId, int amount)
private IReadOnlyDictionary<string, FeatureQuotaStats> GetFeatureQuotaStats(
PatronTier patronTier,
IReadOnlyDictionary<string, PatronQuota>? allQuotasDict,
Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> 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 userCommandQuotaStats = new Dictionary<string, FeatureQuotaStats>();
foreach (var (key, quotaData) in commands)
{
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;
}
// if data is null that means the quota for the user's tier is unlimited
// no point in returning it?
return false;
}
if (data is null)
continue;
private TimeSpan? GetExpiry(QuotaLimit userLimit)
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()
{
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;
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,
};
}
}
private TypedKey<int> CreateKey(LimitedFeatureName key, ulong userId)
=> new($"limited_feature:{key}:{userId}");
return userCommandQuotaStats;
}
private readonly QuotaLimit _emptyQuota = new QuotaLimit()
public async Task<FeatureLimit> TryGetFeatureLimitAsync(FeatureLimitKey key, ulong userId, int? defaultValue)
{
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,
QuotaPeriod = QuotaPer.PerDay,
IsPatronLimit = true,
};
private readonly QuotaLimit _infiniteQuota = new QuotaLimit()
return new()
{
Quota = -1,
QuotaPeriod = QuotaPer.PerDay,
Name = key.PrettyName,
Quota = limit,
IsPatronLimit = true
};
public async Task<QuotaLimit> GetUserLimit(LimitedFeatureName name, ulong userId)
{
if (!_pConf.Data.IsEnabled)
return _infiniteQuota;
var maybePatron = await GetPatronAsync(userId);
if (maybePatron is not { } patron)
return _emptyQuota;
if (patron.ValidThru < DateTime.UtcNow)
return _emptyQuota;
foreach (var (key, value) in _pConf.Data.Limits)
{
if (patron.Amount >= key)
{
if (value.TryGetValue(name, out var quotaLimit))
{
return quotaLimit;
}
break;
}
}
return _emptyQuota;
}
public async Task<Dictionary<LimitedFeatureName, (int, QuotaLimit)>> LimitStats(ulong userId)
{
var dict = new Dictionary<LimitedFeatureName, (int, QuotaLimit)>();
foreach (var featureName in Enum.GetValues<LimitedFeatureName>())
{
var cacheKey = CreateKey(featureName, userId);
var userLimit = await GetUserLimit(featureName, userId);
var cur = await _cache.GetOrAddAsync(cacheKey, () => Task.FromResult(0), GetExpiry(userLimit));
dict[featureName] = (cur, userLimit);
}
return dict;
}
// public async Task<Patron> GiftPatronAsync(IUser user, int amount)
// {
// if (amount < 1)
// throw new ArgumentOutOfRangeException(nameof(amount));
//
//
// }
private Patron PatronUserToPatron(PatronUser user)
=> new Patron()
@ -401,22 +767,6 @@ 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
@ -444,7 +794,7 @@ public sealed class PatronageService
*- **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/>*
*- 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}");

View file

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

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
namespace EllieBot.Modules.Permissions.Services;
public readonly struct ServerFilterSettings

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
using EllieBot.Common.ModuleBehaviors;
namespace EllieBot.Modules.Permissions.Services;

View file

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

View file

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

View file

@ -1,4 +1,4 @@
#nullable disable
#nullable disable
namespace EllieBot.Modules.Permissions.Common;
public class PermissionsCollection<T> : IndexedCollection<T>

View file

@ -149,7 +149,8 @@ public class PermissionService : IExecPreCommand, IEService
returnMsg = "You need Admin permissions in order to use permission commands.";
if (pc.Verbose)
{
try { await _sender.Response(channel).Error(returnMsg).SendAsync(); }
try
{ await _sender.Response(channel).Error(returnMsg).SendAsync(); }
catch { }
}
@ -161,7 +162,8 @@ public class PermissionService : IExecPreCommand, IEService
returnMsg = $"You need the {Format.Bold(role.Name)} role in order to use permission commands.";
if (pc.Verbose)
{
try { await _sender.Response(channel).Error(returnMsg).SendAsync(); }
try
{ await _sender.Response(channel).Error(returnMsg).SendAsync(); }
catch { }
}

View file

@ -130,7 +130,7 @@ public class CryptoService : IEService
await _getCryptoLock.WaitAsync();
try
{
var data = await _cache.GetOrAddAsync(new("nadeko:crypto_data"),
var data = await _cache.GetOrAddAsync(new("ellie:crypto_data"),
async () =>
{
try

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